Files
coolify/tests/Unit/GitRefValidationTest.php
T
Andras BacsaiandClaude Opus 5.5 a79ee909c6 chore: remove unused components and markup-only tests
- 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>
2026-09-28 00:18:16 +02:00

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; #"]);
});
});