fix(backup): validate MongoDB collection names in backup input (#9168)
This commit is contained in:
commit
b1de75a7c6
5 changed files with 150 additions and 16 deletions
|
|
@ -792,6 +792,18 @@ public function create_backup(Request $request)
|
|||
}
|
||||
}
|
||||
|
||||
// Validate databases_to_backup input
|
||||
if (! empty($backupData['databases_to_backup'])) {
|
||||
try {
|
||||
validateDatabasesBackupInput($backupData['databases_to_backup']);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['databases_to_backup' => [$e->getMessage()]],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
// Add required fields
|
||||
$backupData['database_id'] = $database->id;
|
||||
$backupData['database_type'] = $database->getMorphClass();
|
||||
|
|
@ -997,6 +1009,18 @@ public function update_backup(Request $request)
|
|||
unset($backupData['s3_storage_uuid']);
|
||||
}
|
||||
|
||||
// Validate databases_to_backup input
|
||||
if (! empty($backupData['databases_to_backup'])) {
|
||||
try {
|
||||
validateDatabasesBackupInput($backupData['databases_to_backup']);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['databases_to_backup' => [$e->getMessage()]],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
$backupConfig->update($backupData);
|
||||
|
||||
if ($request->backup_now) {
|
||||
|
|
|
|||
|
|
@ -524,10 +524,18 @@ private function backup_standalone_mongodb(string $databaseWithCollections): voi
|
|||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $escapedDatabaseName --gzip --archive > $this->backup_location";
|
||||
}
|
||||
} else {
|
||||
// Validate and escape each collection name
|
||||
$escapedCollections = $collectionsToExclude->map(function ($collection) {
|
||||
$collection = trim($collection);
|
||||
validateShellSafePath($collection, 'collection name');
|
||||
|
||||
return escapeshellarg($collection);
|
||||
});
|
||||
|
||||
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";
|
||||
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$escapedCollections->implode(' --excludeCollection ')." --archive > $this->backup_location";
|
||||
} else {
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $escapedDatabaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
|
||||
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $escapedDatabaseName --gzip --excludeCollection ".$escapedCollections->implode(' --excludeCollection ')." --archive > $this->backup_location";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,21 +105,9 @@ public function syncData(bool $toModel = false)
|
|||
$this->backup->s3_storage_id = $this->s3StorageId;
|
||||
|
||||
// Validate databases_to_backup to prevent command injection
|
||||
// Handles all formats including MongoDB's "db:col1,col2|db2:col3"
|
||||
if (filled($this->databasesToBackup)) {
|
||||
$databases = str($this->databasesToBackup)->explode(',');
|
||||
foreach ($databases as $index => $db) {
|
||||
$dbName = trim($db);
|
||||
try {
|
||||
validateShellSafePath($dbName, 'database name');
|
||||
} catch (\Exception $e) {
|
||||
// Provide specific error message indicating which database failed validation
|
||||
$position = $index + 1;
|
||||
throw new \Exception(
|
||||
"Database #{$position} ('{$dbName}') validation failed: ".
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
validateDatabasesBackupInput($this->databasesToBackup);
|
||||
}
|
||||
|
||||
$this->backup->databases_to_backup = $this->databasesToBackup;
|
||||
|
|
|
|||
|
|
@ -148,6 +148,59 @@ function validateShellSafePath(string $input, string $context = 'path'): string
|
|||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a databases_to_backup input string is safe from command injection.
|
||||
*
|
||||
* Supports all database formats:
|
||||
* - PostgreSQL/MySQL/MariaDB: "db1,db2,db3"
|
||||
* - MongoDB: "db1:col1,col2|db2:col3,col4"
|
||||
*
|
||||
* Validates each database name AND collection name individually against shell metacharacters.
|
||||
*
|
||||
* @param string $input The databases_to_backup string
|
||||
* @return string The validated input
|
||||
*
|
||||
* @throws \Exception If any component contains dangerous characters
|
||||
*/
|
||||
function validateDatabasesBackupInput(string $input): string
|
||||
{
|
||||
// Split by pipe (MongoDB multi-db separator)
|
||||
$databaseEntries = explode('|', $input);
|
||||
|
||||
foreach ($databaseEntries as $entry) {
|
||||
$entry = trim($entry);
|
||||
if ($entry === '' || $entry === 'all' || $entry === '*') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_contains($entry, ':')) {
|
||||
// MongoDB format: dbname:collection1,collection2
|
||||
$databaseName = str($entry)->before(':')->value();
|
||||
$collections = str($entry)->after(':')->explode(',');
|
||||
|
||||
validateShellSafePath($databaseName, 'database name');
|
||||
|
||||
foreach ($collections as $collection) {
|
||||
$collection = trim($collection);
|
||||
if ($collection !== '') {
|
||||
validateShellSafePath($collection, 'collection name');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Simple format: just a database name (may contain commas for non-Mongo)
|
||||
$databases = explode(',', $entry);
|
||||
foreach ($databases as $db) {
|
||||
$db = trim($db);
|
||||
if ($db !== '' && $db !== 'all' && $db !== '*') {
|
||||
validateShellSafePath($db, 'database name');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a string is a safe git ref (commit SHA, branch name, tag, or HEAD).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -81,3 +81,64 @@
|
|||
expect(fn () => validateShellSafePath('test123', 'database name'))
|
||||
->not->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
// --- 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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue