mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
- Delete Blade components, component classes, Livewire components and views that nothing uses after the redesign. - Delete tests that only compare Blade/CSS source text; keep the source checks for authorization gates, stable wire:key values and escaping. - Update stale tests to the current UI and behaviour. - Update DESIGN.md to the current navigation and layout. - Add a lesson: test behaviour, not source markup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
4.9 KiB
PHP
141 lines
4.9 KiB
PHP
<?php
|
|
|
|
use App\Models\Application;
|
|
use App\Models\ApplicationSetting;
|
|
|
|
/**
|
|
* Split a command into words the way /bin/sh does. The words are only printed,
|
|
* so a correctly quoted command cannot run any part of its payload.
|
|
*
|
|
* @return array<int, string>
|
|
*/
|
|
function gitRefShellWords(string $command): array
|
|
{
|
|
$output = shell_exec('for word in '.$command.'; do printf "%s\\0" "$word"; done');
|
|
|
|
return explode("\0", rtrim((string) $output, "\0"));
|
|
}
|
|
|
|
/**
|
|
* Tests for git ref validation.
|
|
*
|
|
* Ensures that git_commit_sha and related inputs are validated
|
|
* to prevent OS command injection via shell metacharacters.
|
|
*/
|
|
describe('validateGitRef', function () {
|
|
test('accepts valid hex commit SHAs', function () {
|
|
expect(validateGitRef('abc123def456'))->toBe('abc123def456');
|
|
expect(validateGitRef('a3e59e5c9'))->toBe('a3e59e5c9');
|
|
expect(validateGitRef('abc123def456abc123def456abc123def456abc123'))->toBe('abc123def456abc123def456abc123def456abc123');
|
|
});
|
|
|
|
test('accepts HEAD', function () {
|
|
expect(validateGitRef('HEAD'))->toBe('HEAD');
|
|
});
|
|
|
|
test('accepts empty string', function () {
|
|
expect(validateGitRef(''))->toBe('');
|
|
});
|
|
|
|
test('accepts branch and tag names', function () {
|
|
expect(validateGitRef('main'))->toBe('main');
|
|
expect(validateGitRef('feature/my-branch'))->toBe('feature/my-branch');
|
|
expect(validateGitRef('v1.2.3'))->toBe('v1.2.3');
|
|
expect(validateGitRef('release-2.0'))->toBe('release-2.0');
|
|
expect(validateGitRef('my_branch'))->toBe('my_branch');
|
|
});
|
|
|
|
test('trims whitespace', function () {
|
|
expect(validateGitRef(' abc123 '))->toBe('abc123');
|
|
});
|
|
|
|
test('rejects single quote injection', function () {
|
|
expect(fn () => validateGitRef("HEAD'; id >/tmp/poc; #"))
|
|
->toThrow(Exception::class);
|
|
});
|
|
|
|
test('rejects semicolon command separator', function () {
|
|
expect(fn () => validateGitRef('abc123; rm -rf /'))
|
|
->toThrow(Exception::class);
|
|
});
|
|
|
|
test('rejects command substitution with $()', function () {
|
|
expect(fn () => validateGitRef('$(whoami)'))
|
|
->toThrow(Exception::class);
|
|
});
|
|
|
|
test('rejects backtick command substitution', function () {
|
|
expect(fn () => validateGitRef('`whoami`'))
|
|
->toThrow(Exception::class);
|
|
});
|
|
|
|
test('rejects pipe operator', function () {
|
|
expect(fn () => validateGitRef('abc | cat /etc/passwd'))
|
|
->toThrow(Exception::class);
|
|
});
|
|
|
|
test('rejects ampersand operator', function () {
|
|
expect(fn () => validateGitRef('abc & whoami'))
|
|
->toThrow(Exception::class);
|
|
});
|
|
|
|
test('rejects hash comment injection', function () {
|
|
expect(fn () => validateGitRef('abc #'))
|
|
->toThrow(Exception::class);
|
|
});
|
|
|
|
test('rejects newline injection', function () {
|
|
expect(fn () => validateGitRef("abc\nwhoami"))
|
|
->toThrow(Exception::class);
|
|
});
|
|
|
|
test('rejects redirect operators', function () {
|
|
expect(fn () => validateGitRef('abc > /tmp/out'))
|
|
->toThrow(Exception::class);
|
|
});
|
|
|
|
test('rejects hyphen-prefixed input (git flag injection)', function () {
|
|
expect(fn () => validateGitRef('--upload-pack=malicious'))
|
|
->toThrow(Exception::class);
|
|
});
|
|
|
|
test('rejects the exact PoC payload from advisory', function () {
|
|
expect(fn () => validateGitRef("HEAD'; whoami >/tmp/coolify_poc_git; #"))
|
|
->toThrow(Exception::class);
|
|
});
|
|
});
|
|
|
|
describe('executeInDocker git log escaping', function () {
|
|
test('git log command escapes commit SHA to prevent injection', function () {
|
|
$maliciousCommit = "HEAD'; id; #";
|
|
$command = 'cd /workdir && git log -1 '.escapeshellarg($maliciousCommit).' --pretty=%B';
|
|
$result = executeInDocker('test-container', $command);
|
|
|
|
// The whole git command stays one bash -c argument.
|
|
expect(gitRefShellWords($result))->toBe(['docker', 'exec', 'test-container', 'bash', '-c', $command]);
|
|
|
|
// Inside bash -c, the malicious payload stays one git log argument.
|
|
expect(gitRefShellWords(str($command)->after('&& ')->toString()))
|
|
->toBe(['git', 'log', '-1', $maliciousCommit, '--pretty=%B']);
|
|
});
|
|
});
|
|
|
|
describe('buildGitCheckoutCommand escaping', function () {
|
|
test('checkout command escapes target to prevent injection', function () {
|
|
$app = new Application;
|
|
$app->fill(['uuid' => 'test-uuid']);
|
|
|
|
$settings = new ApplicationSetting;
|
|
$settings->is_git_submodules_enabled = false;
|
|
$app->setRelation('settings', $settings);
|
|
|
|
$method = new ReflectionMethod($app, 'buildGitCheckoutCommand');
|
|
|
|
$result = $method->invoke($app, 'abc123');
|
|
expect($result)->toContain("git checkout 'abc123'");
|
|
|
|
$result = $method->invoke($app, "abc'; id; #");
|
|
expect(gitRefShellWords($result))->toBe(['git', 'checkout', "abc'; id; #"]);
|
|
});
|
|
});
|