mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 12:16:45 +00:00
fix(database): tighten Postgres init script filename handling
Validate new init-script filenames against path traversal and shell metacharacters via a new validateFilenameSafe() helper, and harden the write/delete paths with basename() + escapeshellarg() so legacy rows still deploy and can be cleaned up without regressions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1a78df58e
commit
a05d4e3a4b
5 files changed
+297
-10
No files matched your search
@@ -74,3 +74,69 @@ test('postgresql init script accepts legitimate filenames', function () {
|
||||
expect(fn () => validateShellSafePath('setup_db.sql', 'init script filename'))
|
||||
->not->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
// Path traversal — GHSA-mv4c-9x67-rrmv regression tests
|
||||
test('postgresql init script rejects path traversal with ../ sequence', function () {
|
||||
expect(fn () => validateFilenameSafe('../../../etc/cron.d/pwn', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('postgresql init script rejects path traversal targeting /etc/cron.d', function () {
|
||||
expect(fn () => validateFilenameSafe('../../../../../etc/cron.d/k4zrce', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('postgresql init script rejects absolute path', function () {
|
||||
expect(fn () => validateFilenameSafe('/etc/passwd', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('postgresql init script rejects filename with forward slash', function () {
|
||||
expect(fn () => validateFilenameSafe('subdir/evil.sql', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('postgresql init script rejects filename with backslash', function () {
|
||||
expect(fn () => validateFilenameSafe('subdir\\evil.sql', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('postgresql init script rejects double-dot without slashes', function () {
|
||||
expect(fn () => validateFilenameSafe('..', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('postgresql init script rejects null byte injection', function () {
|
||||
expect(fn () => validateFilenameSafe("init.sql\0../../etc/passwd", 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('postgresql init script accepts legitimate filenames via validateFilenameSafe', function () {
|
||||
expect(fn () => validateFilenameSafe('init.sql', 'init script filename'))
|
||||
->not->toThrow(Exception::class);
|
||||
|
||||
expect(fn () => validateFilenameSafe('01_schema.sql', 'init script filename'))
|
||||
->not->toThrow(Exception::class);
|
||||
|
||||
expect(fn () => validateFilenameSafe('init-script.sh', 'init script filename'))
|
||||
->not->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
// Write-site defence — basename() + escapeshellarg() keep legacy/bad rows safe
|
||||
test('basename() strips path traversal from legacy filenames at write site', function () {
|
||||
expect(basename('../../../etc/cron.d/pwn'))->toBe('pwn');
|
||||
expect(basename('/etc/passwd'))->toBe('passwd');
|
||||
expect(basename('subdir/evil.sql'))->toBe('evil.sql');
|
||||
});
|
||||
|
||||
test('escapeshellarg() neutralises shell metacharacters in tee target', function () {
|
||||
// Simulates how StartPostgresql::generate_init_scripts() builds the tee argument
|
||||
$configuration_dir = '/data/coolify/databases/abc123';
|
||||
$legacy_filename = basename('foo bar*.sql;rm -rf /');
|
||||
$target = "$configuration_dir/docker-entrypoint-initdb.d/{$legacy_filename}";
|
||||
$escaped = escapeshellarg($target);
|
||||
|
||||
// Single-quoted in POSIX sh means no expansion / no extra args regardless of contents.
|
||||
expect($escaped)->toStartWith("'")->toEndWith("'");
|
||||
expect($escaped)->toContain('foo bar*.sql;rm -rf');
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
test('allows plain filenames without special characters', function () {
|
||||
$validNames = [
|
||||
'init.sql',
|
||||
'01_schema.sql',
|
||||
'setup-db.sql',
|
||||
'create_test_db.sql',
|
||||
'init-script.sh',
|
||||
'UPPERCASE.SQL',
|
||||
'mixed_Case-File.sql',
|
||||
'file123.sql',
|
||||
'a',
|
||||
];
|
||||
|
||||
foreach ($validNames as $name) {
|
||||
expect(fn () => validateFilenameSafe($name, 'init script filename'))
|
||||
->not->toThrow(Exception::class, "Expected '{$name}' to pass");
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects path traversal with ../', function () {
|
||||
expect(fn () => validateFilenameSafe('../../../etc/cron.d/pwn', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects path traversal with .. alone', function () {
|
||||
expect(fn () => validateFilenameSafe('..', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects path traversal embedded in filename', function () {
|
||||
expect(fn () => validateFilenameSafe('foo..bar', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects forward slash directory separator', function () {
|
||||
expect(fn () => validateFilenameSafe('foo/bar.sql', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects backslash directory separator', function () {
|
||||
expect(fn () => validateFilenameSafe('foo\\bar.sql', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects absolute path starting with slash', function () {
|
||||
expect(fn () => validateFilenameSafe('/etc/passwd', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects absolute Windows-style path', function () {
|
||||
expect(fn () => validateFilenameSafe('C:\\Windows\\System32\\cmd.exe', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects null byte injection', function () {
|
||||
expect(fn () => validateFilenameSafe("init.sql\0../../etc/passwd", 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects shell command substitution (inherits from validateShellSafePath)', function () {
|
||||
expect(fn () => validateFilenameSafe('$(whoami).sql', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects backtick command substitution', function () {
|
||||
expect(fn () => validateFilenameSafe('`id`.sql', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects semicolon command separator', function () {
|
||||
expect(fn () => validateFilenameSafe('init.sql;rm -rf /', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects pipe operator', function () {
|
||||
expect(fn () => validateFilenameSafe('init.sql|whoami', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects redirect operators', function () {
|
||||
expect(fn () => validateFilenameSafe('init.sql>/etc/passwd', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects mixed traversal and shell injection', function () {
|
||||
expect(fn () => validateFilenameSafe('../etc/cron.d/$(id)', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('error message contains context string', function () {
|
||||
try {
|
||||
validateFilenameSafe('../evil', 'init script filename');
|
||||
expect(false)->toBeTrue('Should have thrown');
|
||||
} catch (Exception $e) {
|
||||
expect($e->getMessage())->toContain('init script filename');
|
||||
}
|
||||
});
|
||||
|
||||
test('handles empty string without throwing', function () {
|
||||
expect(fn () => validateFilenameSafe('', 'init script filename'))
|
||||
->not->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects whitespace inside filename (would split into extra tee arg)', function () {
|
||||
expect(fn () => validateFilenameSafe('foo bar.sql', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects glob wildcards', function () {
|
||||
expect(fn () => validateFilenameSafe('init*.sql', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
|
||||
expect(fn () => validateFilenameSafe('init?.sql', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects glob character class brackets', function () {
|
||||
expect(fn () => validateFilenameSafe('init[abc].sql', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects tilde expansion', function () {
|
||||
expect(fn () => validateFilenameSafe('~/evil.sql', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
|
||||
expect(fn () => validateFilenameSafe('~root', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('rejects single and double quotes', function () {
|
||||
expect(fn () => validateFilenameSafe("foo'bar.sql", 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
|
||||
expect(fn () => validateFilenameSafe('foo"bar.sql', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
Reference in new issue
Block a user