feat: sanitize configuration.php in backups
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 4s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 8s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled

Strip sensitive credentials from configuration.php before adding it
to the backup archive. Replaced fields use [SANITIZED:field] placeholders:
- Database: host, user, password, db
- Security: secret
- SMTP: smtpuser, smtppass, smtphost
- Proxy: proxy_user, proxy_pass
- Redis: redis_server_auth, session_redis_server_auth
- DB TLS: dbsslkey, dbsslcert, dbsslca

Non-sensitive fields (sitename, debug, cache, SEF, paths, etc.)
are preserved as-is for inspection and partial restores.

MokoRestore detects sanitized placeholders and leaves those form
fields blank so the user must enter fresh credentials (like the
Joomla installer). In-Joomla restore is unaffected because
RestoreEngine preserves the current site's configuration.php.

Applied to both BackupEngine (synchronous) and SteppedBackupEngine
(AJAX-based) code paths.
This commit is contained in:
Jonathan Miller
2026-06-14 14:42:49 -05:00
parent 88b930835a
commit 2f490c3208
3 changed files with 93 additions and 24 deletions
@@ -161,10 +161,20 @@ class BackupEngine
foreach ($filesToBackup as $relativePath) {
$fullPath = JPATH_ROOT . '/' . $relativePath;
if (is_file($fullPath) && is_readable($fullPath)) {
$archiver->addFile($fullPath, $relativePath);
} else {
if (!is_file($fullPath) || !is_readable($fullPath)) {
$skippedFiles++;
continue;
}
// Sanitize configuration.php — replace credentials with
// placeholders so backups are safe to store or transfer.
// MokoRestore prompts for these values during restore.
if ($relativePath === 'configuration.php') {
$sanitized = self::sanitizeConfiguration($fullPath);
$archiver->addFromString($relativePath, $sanitized);
$this->log('configuration.php sanitized (credentials replaced with placeholders)');
} else {
$archiver->addFile($fullPath, $relativePath);
}
}
@@ -494,6 +504,58 @@ class BackupEngine
}
}
/**
* Sanitize configuration.php by replacing sensitive field values with
* [SANITIZED:fieldname] placeholders. Non-sensitive fields (sitename,
* debug, cache, SEF, etc.) are preserved as-is.
*
* @param string $path Absolute path to configuration.php
*
* @return string Sanitized file contents
*/
public static function sanitizeConfiguration(string $path): string
{
$content = file_get_contents($path);
if ($content === false) {
return '';
}
// Fields whose values must be replaced with placeholders.
// Grouped by category for maintainability.
$sensitiveFields = [
// Database
'host', 'user', 'password', 'db',
// Security
'secret',
// SMTP
'smtpuser', 'smtppass', 'smtphost',
// Proxy
'proxy_user', 'proxy_pass',
// Redis
'redis_server_auth', 'session_redis_server_auth',
// Database TLS
'dbsslkey', 'dbsslcert', 'dbsslca',
];
foreach ($sensitiveFields as $field) {
// Match: public $field = 'value'; (single-quoted)
$content = preg_replace(
'/^(\s*public\s+\$' . preg_quote($field, '/') . '\s*=\s*\').*?(\';)/m',
'$1[SANITIZED:' . $field . ']$2',
$content
);
// Match: public $field = "value"; (double-quoted)
$content = preg_replace(
'/^(\s*public\s+\$' . preg_quote($field, '/') . '\s*=\s*").*?("\s*;)/m',
'$1[SANITIZED:' . $field . ']$2',
$content
);
}
return $content;
}
private function log(string $message): void
{
$this->log[] = '[' . date('H:i:s') . '] ' . $message;
@@ -258,31 +258,29 @@ function actionExtract(array $data): array
$count = $zip->numFiles;
$zip->close();
// Try to read existing configuration.php for pre-filling
// Try to read existing configuration.php for pre-filling.
// Sanitized backups contain [SANITIZED:field] placeholders — skip those.
$existingConfig = [];
$configFile = RESTORE_DIR . '/configuration.php';
if (is_file($configFile)) {
$content = file_get_contents($configFile);
if (preg_match('/\$host\s*=\s*\'([^\']*)\'/', $content, $m)) {
$existingConfig['db_host'] = $m[1];
}
$fieldMap = [
'host' => 'db_host',
'db' => 'db_name',
'user' => 'db_user',
'dbprefix' => 'db_prefix',
'sitename' => 'sitename',
];
if (preg_match('/\$db\s*=\s*\'([^\']*)\'/', $content, $m)) {
$existingConfig['db_name'] = $m[1];
}
if (preg_match('/\$user\s*=\s*\'([^\']*)\'/', $content, $m)) {
$existingConfig['db_user'] = $m[1];
}
if (preg_match('/\$dbprefix\s*=\s*\'([^\']*)\'/', $content, $m)) {
$existingConfig['db_prefix'] = $m[1];
}
if (preg_match('/\$sitename\s*=\s*\'([^\']*)\'/', $content, $m)) {
$existingConfig['sitename'] = $m[1];
foreach ($fieldMap as $phpField => $configKey) {
if (preg_match('/\$' . preg_quote($phpField, '/') . '\s*=\s*\'([^\']*)\'/', $content, $m)) {
// Skip sanitized placeholder values
if (strpos($m[1], '[SANITIZED:') === false) {
$existingConfig[$configKey] = $m[1];
}
}
}
}
@@ -277,10 +277,19 @@ class SteppedBackupEngine
foreach ($batch as $relativePath) {
$fullPath = JPATH_ROOT . '/' . $relativePath;
if (is_file($fullPath) && is_readable($fullPath)) {
$zip->addFile($fullPath, $relativePath);
$added++;
if (!is_file($fullPath) || !is_readable($fullPath)) {
continue;
}
// Sanitize Joomla config — replace credentials with placeholders
if (basename($relativePath) === 'configuration.php' && dirname($relativePath) === '.') {
$sanitized = BackupEngine::sanitizeConfiguration($fullPath);
$zip->addFromString($relativePath, $sanitized);
} else {
$zip->addFile($fullPath, $relativePath);
}
$added++;
}
$zip->close();