diff --git a/exec/lib/BackupQueueService.php b/exec/lib/BackupQueueService.php index bc36136..405746e 100644 --- a/exec/lib/BackupQueueService.php +++ b/exec/lib/BackupQueueService.php @@ -437,10 +437,12 @@ final class BackupQueueService throw new RuntimeException('Nie można odczytać przesłanego pliku backupu.'); } + // Security: never trust $uploadReference as a literal path - it comes + // straight from user-controlled POST data, and this whole plugin runs + // as root. Only accept a file whose *basename* matches one of the + // known DirectAdmin/system temp directories below, never an + // arbitrary absolute path chosen by the caller. $sourceCandidates = []; - if (str_starts_with($uploadReference, '/') && is_file($uploadReference)) { - $sourceCandidates[] = $uploadReference; - } $tmpDirs = [ (string)(getenv('UPLOAD_TMP_DIR') ?: ''), (string)(getenv('DA_UPLOAD_TMP_DIR') ?: ''), diff --git a/tests/backup_restore_upload_path_test.php b/tests/backup_restore_upload_path_test.php new file mode 100644 index 0000000..7bc6efa --- /dev/null +++ b/tests/backup_restore_upload_path_test.php @@ -0,0 +1,96 @@ +newInstanceWithoutConstructor(); + foreach ([ + 'username' => $username, + 'prefix' => $username . '_', + 'userConf' => [], + 'packageConf' => [], + 'settings' => $settings, + ] as $property => $value) { + $prop = $class->getProperty($property); + $prop->setAccessible(true); + $prop->setValue($user, $value); + } + return $user; +} + +$settings = load_settings(''); +$daUser = make_da_user($settings, 'demo'); +$queue = new BackupQueueService($settings, $daUser); + +// A path outside every recognized DirectAdmin/system temp directory must be +// rejected, even if it exists and is readable - it must not be treated as a +// legitimate upload reference just because the caller supplied it verbatim. +$secretDir = sys_get_temp_dir() . '/altmysql-secret-' . bin2hex(random_bytes(4)); +mkdir($secretDir, 0700, true); +$secretFile = $secretDir . '/secret_credentials.sql'; +file_put_contents($secretFile, 'SECRET CONTENT THAT MUST NEVER BE COPIED'); + +$threw = false; +try { + $queue->stageRestoreUploadFromDirectAdminTemp($secretFile, 'harmless.sql'); +} catch (Throwable $e) { + $threw = true; +} +assert_true($threw, 'a path outside the known DirectAdmin/system temp directories must be rejected, not staged'); + +// A legitimate reference - a file that DirectAdmin actually placed directly +// inside a recognized temp directory - must still work. +$legitFile = sys_get_temp_dir() . '/altmysql-da-upload-' . bin2hex(random_bytes(4)) . '.sql'; +file_put_contents($legitFile, 'SELECT 1;'); + +$stagedPath = $queue->stageRestoreUploadFromDirectAdminTemp($legitFile, 'legit.sql'); +assert_true(is_file($stagedPath), 'a file directly inside a recognized temp directory must still be staged'); +assert_true( + trim((string)file_get_contents($stagedPath)) === 'SELECT 1;', + 'the staged file must contain the legitimate source content' +); + +// cleanup +@unlink($secretFile); +@rmdir($secretDir); +@unlink($legitFile); +@unlink($stagedPath); +exec('rm -rf ' . escapeshellarg($pluginTempRoot)); + +echo "backup_restore_upload_path_test: OK\n";