fix: final review — SQL injection, input escaping, undefined var
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
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 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 3s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 6s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 20s
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
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
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 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 3s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 6s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 20s
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
Critical/High: - Fix undefined $configFile → $configPath in from-scratch config path - Escape all user input with addcslashes before interpolating into configuration.php (both regex-replace and HEREDOC paths) - Add getValidatedPrefix() helper — validates db_prefix format before use in SQL table names across all restore functions - fixPackageClientId() now warns user via enqueueMessage on failure - sanitizeConfiguration() logs error on file read failure Medium: - Content-Disposition header uses RFC 6266 rawurlencode (both admin and API download controllers) - Remove @unlink suppression, log warning on failure - viewLog() catch block now logs exception context - writeDefaultHtaccess() checks copy/write, returns status to caller - actionConfig() checks file_put_contents return value
This commit is contained in:
@@ -92,7 +92,7 @@ class BackupsController extends ApiController
|
||||
: 'application/zip';
|
||||
|
||||
header('Content-Type: ' . $contentType);
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($filename));
|
||||
header('Content-Length: ' . $filesize);
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class BackupsController extends AdminController
|
||||
: 'application/zip';
|
||||
|
||||
header('Content-Type: ' . $contentType);
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($filename));
|
||||
header('Content-Length: ' . $filesize);
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
@@ -411,26 +411,38 @@ function actionConfig(array $data): array
|
||||
$config = file_get_contents($basePath);
|
||||
|
||||
// Replace all credential and server-specific fields with user input
|
||||
// Escape all user input for safe interpolation into PHP string literals
|
||||
$eHost = addcslashes($host, "'\\");
|
||||
$eDbName = addcslashes($dbName, "'\\");
|
||||
$eDbUser = addcslashes($dbUser, "'\\");
|
||||
$eDbPass = addcslashes($dbPass, "'\\");
|
||||
$ePrefix = addcslashes($prefix, "'\\");
|
||||
$eSite = addcslashes($sitename, "'\\");
|
||||
$eLive = addcslashes($livesite, "'\\");
|
||||
$eSmtpH = addcslashes($smtpHost, "'\\");
|
||||
$eSmtpU = addcslashes($smtpUser, "'\\");
|
||||
$eSmtpP = addcslashes($smtpPass, "'\\");
|
||||
|
||||
$replacements = [
|
||||
'/\$host\s*=\s*\'[^\']*\'/' => "\$host = '{$host}'",
|
||||
'/\$db\s*=\s*\'[^\']*\'/' => "\$db = '{$dbName}'",
|
||||
'/\$user\s*=\s*\'[^\']*\'/' => "\$user = '{$dbUser}'",
|
||||
'/\$password\s*=\s*\'[^\']*\'/' => "\$password = '" . addcslashes($dbPass, "'\\") . "'",
|
||||
'/\$dbprefix\s*=\s*\'[^\']*\'/' => "\$dbprefix = '{$prefix}'",
|
||||
'/\$host\s*=\s*\'[^\']*\'/' => "\$host = '{$eHost}'",
|
||||
'/\$db\s*=\s*\'[^\']*\'/' => "\$db = '{$eDbName}'",
|
||||
'/\$user\s*=\s*\'[^\']*\'/' => "\$user = '{$eDbUser}'",
|
||||
'/\$password\s*=\s*\'[^\']*\'/' => "\$password = '{$eDbPass}'",
|
||||
'/\$dbprefix\s*=\s*\'[^\']*\'/' => "\$dbprefix = '{$ePrefix}'",
|
||||
'/\$tmp_path\s*=\s*\'[^\']*\'/' => "\$tmp_path = '{$tmpPath}'",
|
||||
'/\$log_path\s*=\s*\'[^\']*\'/' => "\$log_path = '{$logPath}'",
|
||||
'/\$sitename\s*=\s*\'[^\']*\'/' => "\$sitename = '" . addcslashes($sitename, "'\\") . "'",
|
||||
'/\$sitename\s*=\s*\'[^\']*\'/' => "\$sitename = '{$eSite}'",
|
||||
'/\$secret\s*=\s*\'[^\']*\'/' => "\$secret = '" . bin2hex(random_bytes(16)) . "'",
|
||||
];
|
||||
|
||||
if ($livesite !== '') {
|
||||
$replacements['/\$live_site\s*=\s*\'[^\']*\'/'] = "\$live_site = '{$livesite}'";
|
||||
$replacements['/\$live_site\s*=\s*\'[^\']*\'/'] = "\$live_site = '{$eLive}'";
|
||||
}
|
||||
|
||||
// SMTP — always replace (clears sanitized placeholders even if blank)
|
||||
$replacements['/\$smtphost\s*=\s*\'[^\']*\'/'] = "\$smtphost = '" . addcslashes($smtpHost, "'\\") . "'";
|
||||
$replacements['/\$smtpuser\s*=\s*\'[^\']*\'/'] = "\$smtpuser = '" . addcslashes($smtpUser, "'\\") . "'";
|
||||
$replacements['/\$smtppass\s*=\s*\'[^\']*\'/'] = "\$smtppass = '" . addcslashes($smtpPass, "'\\") . "'";
|
||||
$replacements['/\$smtphost\s*=\s*\'[^\']*\'/'] = "\$smtphost = '{$eSmtpH}'";
|
||||
$replacements['/\$smtpuser\s*=\s*\'[^\']*\'/'] = "\$smtpuser = '{$eSmtpU}'";
|
||||
$replacements['/\$smtppass\s*=\s*\'[^\']*\'/'] = "\$smtppass = '{$eSmtpP}'";
|
||||
|
||||
// Clear remaining sanitized placeholders (proxy, Redis, DB TLS)
|
||||
$replacements['/\$proxy_user\s*=\s*\'[^\']*\'/'] = "\$proxy_user = ''";
|
||||
@@ -470,8 +482,15 @@ function actionConfig(array $data): array
|
||||
return ['success' => true, 'message' => $msg];
|
||||
}
|
||||
|
||||
// Create new configuration.php from scratch
|
||||
$secret = bin2hex(random_bytes(16));
|
||||
// Create new configuration.php from scratch — use escaped values
|
||||
$eHost = addcslashes($host, "'\\");
|
||||
$eDbName = addcslashes($dbName, "'\\");
|
||||
$eDbUser = addcslashes($dbUser, "'\\");
|
||||
$eDbPass = addcslashes($dbPass, "'\\");
|
||||
$ePrefix = addcslashes($prefix, "'\\");
|
||||
$eSite = addcslashes($sitename, "'\\");
|
||||
$eLive = addcslashes($livesite, "'\\");
|
||||
$secret = bin2hex(random_bytes(16));
|
||||
$newConfig = <<<JCONFIG
|
||||
<?php
|
||||
class JConfig {
|
||||
@@ -479,7 +498,7 @@ class JConfig {
|
||||
public \$offline_message = 'This site is down for maintenance.<br>Please check back again soon.';
|
||||
public \$display_offline_message = 1;
|
||||
public \$offline_image = '';
|
||||
public \$sitename = '{$sitename}';
|
||||
public \$sitename = '{$eSite}';
|
||||
public \$editor = 'tinymce';
|
||||
public \$captcha = '0';
|
||||
public \$list_limit = 20;
|
||||
@@ -488,11 +507,11 @@ class JConfig {
|
||||
public \$debug_lang = false;
|
||||
public \$debug_lang_const = true;
|
||||
public \$dbtype = 'mysqli';
|
||||
public \$host = '{$host}';
|
||||
public \$user = '{$dbUser}';
|
||||
public \$password = '{$dbPass}';
|
||||
public \$db = '{$dbName}';
|
||||
public \$dbprefix = '{$prefix}';
|
||||
public \$host = '{$eHost}';
|
||||
public \$user = '{$eDbUser}';
|
||||
public \$password = '{$eDbPass}';
|
||||
public \$db = '{$eDbName}';
|
||||
public \$dbprefix = '{$ePrefix}';
|
||||
public \$dbencryption = 0;
|
||||
public \$dbsslverifyservercert = false;
|
||||
public \$dbsslkey = '';
|
||||
@@ -500,7 +519,7 @@ class JConfig {
|
||||
public \$dbsslca = '';
|
||||
public \$dbsslcipher = '';
|
||||
public \$force_ssl = 0;
|
||||
public \$live_site = '{$livesite}';
|
||||
public \$live_site = '{$eLive}';
|
||||
public \$secret = '{$secret}';
|
||||
public \$gzip = false;
|
||||
public \$error_reporting = 'default';
|
||||
@@ -514,7 +533,7 @@ class JConfig {
|
||||
}
|
||||
JCONFIG;
|
||||
|
||||
if (file_put_contents($configFile, $newConfig) === false) {
|
||||
if (file_put_contents($configPath, $newConfig) === false) {
|
||||
return ['success' => false, 'message' => 'Failed to write Joomla config file — check directory permissions'];
|
||||
}
|
||||
|
||||
@@ -654,10 +673,21 @@ HTACCESS;
|
||||
return '';
|
||||
}
|
||||
|
||||
function getValidatedPrefix(array $data): string
|
||||
{
|
||||
$prefix = getValidatedPrefix($data);
|
||||
|
||||
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]{0,20}$/', $prefix)) {
|
||||
throw new RuntimeException('Invalid table prefix format');
|
||||
}
|
||||
|
||||
return $prefix;
|
||||
}
|
||||
|
||||
function actionListAdmins(array $data): array
|
||||
{
|
||||
$pdo = getDbConnection($data);
|
||||
$prefix = $data['db_prefix'] ?? 'moko_';
|
||||
$prefix = getValidatedPrefix($data);
|
||||
|
||||
// Find super admin users (group 8 = Super Users in Joomla)
|
||||
$stmt = $pdo->prepare(
|
||||
@@ -703,7 +733,7 @@ function actionResetAdmin(array $data): array
|
||||
function actionProvision(array $data): array
|
||||
{
|
||||
$pdo = getDbConnection($data);
|
||||
$prefix = $data['db_prefix'] ?? 'moko_';
|
||||
$prefix = getValidatedPrefix($data);
|
||||
$tasks = json_decode($data['tasks'] ?? '[]', true) ?: [];
|
||||
$results = [];
|
||||
|
||||
@@ -806,12 +836,8 @@ function getDbConnection(array $data): PDO
|
||||
$user = $data['db_user'] ?? '';
|
||||
$pass = $data['db_pass'] ?? '';
|
||||
|
||||
// Validate db_prefix to prevent SQL injection
|
||||
$prefix = $data['db_prefix'] ?? 'moko_';
|
||||
|
||||
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]{0,20}$/', $prefix)) {
|
||||
throw new RuntimeException('Invalid table prefix format');
|
||||
}
|
||||
// Validate db_prefix to prevent SQL injection (used by callers for table names)
|
||||
getValidatedPrefix($data);
|
||||
|
||||
return new PDO(
|
||||
"mysql:host={$host};dbname={$name};charset=utf8mb4",
|
||||
|
||||
Reference in New Issue
Block a user