Fix phpMyAdmin SSO login and harden SSO/security posture
phpMyAdmin login was broken because phpmyadmin_install.sh never wired the private phpMyAdmin copy into the web server: no Apache Alias was registered via DirectAdmin CustomBuild, so the SSO redirect target was unreachable. Add configure_apache_alias()/apply_apache_alias() (Alias + Directory blocks, ./build rewrite_confs, httpd reload), detect the real Apache group instead of hardcoding diradmin:diradmin, stop silently swallowing chown/chmod failures, and verify an optional SHA256 for the downloaded phpMyAdmin tarball. Extend phpmyadmin_health_check.sh to detect a missing/incorrect Apache alias and to probe HTTP reachability. Security hardening: scope temporary phpMyAdmin SSO MySQL roles to 127.0.0.1 instead of '%', tighten SSO ticket file permissions to 0640 (they contain a plaintext MySQL password), and log MySQL connection failures for diagnosis instead of swallowing them silently. Bump version to 1.2.12 and rebuild the release archive.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$pluginDir = dirname(__DIR__);
|
||||
require_once $pluginDir . '/exec/lib/Settings.php';
|
||||
require_once $pluginDir . '/exec/lib/MySQLService.php';
|
||||
|
||||
function fail(string $message): void
|
||||
{
|
||||
fwrite(STDERR, "FAIL: {$message}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function assert_true(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
fail($message);
|
||||
}
|
||||
}
|
||||
|
||||
$logPath = tempnam(sys_get_temp_dir(), 'altmysql-error-log-');
|
||||
if ($logPath === false) {
|
||||
fail('cannot create temp log file');
|
||||
}
|
||||
ini_set('log_errors', '1');
|
||||
ini_set('error_log', $logPath);
|
||||
|
||||
$settingsPath = tempnam(sys_get_temp_dir(), 'altmysql-settings-');
|
||||
file_put_contents($settingsPath, '');
|
||||
$settings = Settings::load($settingsPath);
|
||||
unlink($settingsPath);
|
||||
|
||||
$mysql = new MySQLService([
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 1,
|
||||
'database' => 'mysql',
|
||||
'user' => 'nobody',
|
||||
'password' => 'nopassword',
|
||||
'socket' => '',
|
||||
], $settings);
|
||||
|
||||
$threw = false;
|
||||
try {
|
||||
$mysql->serverVersion();
|
||||
} catch (Throwable $e) {
|
||||
$threw = true;
|
||||
}
|
||||
assert_true($threw, 'connecting to a closed port must still raise an exception');
|
||||
|
||||
$logContents = (string)file_get_contents($logPath);
|
||||
assert_true(
|
||||
str_contains($logContents, 'MYSQL_CONNECT_FAIL'),
|
||||
'a failed MySQL connection must be logged for diagnosis (expected MYSQL_CONNECT_FAIL marker in error log, got: ' . $logContents . ')'
|
||||
);
|
||||
assert_true(
|
||||
str_contains($logContents, '127.0.0.1'),
|
||||
'the connect-failure log line must include the target host for diagnosis'
|
||||
);
|
||||
|
||||
unlink($logPath);
|
||||
|
||||
echo "mysql_connect_failure_test: OK\n";
|
||||
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
ARCHIVE="${1:-$(cd "$PLUGIN_DIR/.." && pwd)/archives/1.2.11/alt-mysql.tar.gz}"
|
||||
ARCHIVE="${1:-$(cd "$PLUGIN_DIR/.." && pwd)/archives/1.2.12/alt-mysql.tar.gz}"
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
@@ -37,7 +37,7 @@ fi
|
||||
CONF="$(tar -xOzf "$ARCHIVE" ./plugin.conf 2>/dev/null || tar -xOzf "$ARCHIVE" plugin.conf)"
|
||||
printf '%s\n' "$CONF" | grep -Fxq "name=alt-mysql" || fail "archive plugin name is not alt-mysql"
|
||||
printf '%s\n' "$CONF" | grep -Fxq "id=alt-mysql" || fail "archive plugin id is not alt-mysql"
|
||||
printf '%s\n' "$CONF" | grep -Fxq "version=1.2.11" || fail "archive plugin version is not 1.2.11"
|
||||
printf '%s\n' "$CONF" | grep -Fxq "version=1.2.12" || fail "archive plugin version is not 1.2.12"
|
||||
|
||||
SETTINGS="$(tar -xOzf "$ARCHIVE" ./plugin-settings.conf 2>/dev/null || tar -xOzf "$ARCHIVE" plugin-settings.conf)"
|
||||
printf '%s\n' "$SETTINGS" | grep -Fxq "PHPMYADMIN_INSTALL_DIR=/var/www/html/alt-mysql-phpmyadmin" \
|
||||
|
||||
@@ -16,21 +16,19 @@ grep -Fq 'PHPMYADMIN_VERSION="${PHPMYADMIN_VERSION:-5.2.3}"' "$SCRIPT" \
|
||||
grep -Fq 'PHPMYADMIN_TARBALL_URL="${PHPMYADMIN_TARBALL_URL:-https://files.phpmyadmin.net/phpMyAdmin/5.2.3/phpMyAdmin-5.2.3-all-languages.tar.gz}"' "$SCRIPT" \
|
||||
|| fail "phpMyAdmin tarball URL is not the explicit 5.2.3 URL"
|
||||
|
||||
mkdir -p "$TMP_DIR/phpMyAdmin"
|
||||
cat > "$TMP_DIR/phpMyAdmin/config.inc.php" <<'PHP'
|
||||
<?php
|
||||
$cfg = [];
|
||||
PHP
|
||||
mkdir -p "$TMP_DIR/phpMyAdmin-source"
|
||||
cat > "$TMP_DIR/phpMyAdmin-source/index.php" <<'PHP'
|
||||
<?php
|
||||
echo 'private phpMyAdmin';
|
||||
PHP
|
||||
|
||||
PHPMYADMIN_EXISTING_DIR="$TMP_DIR/phpMyAdmin" \
|
||||
CUSTOMBUILD_DIR="$TMP_DIR/custombuild"
|
||||
mkdir -p "$CUSTOMBUILD_DIR"
|
||||
|
||||
PHPMYADMIN_PRIVATE_DIR="$TMP_DIR/alt-mysql-phpmyadmin" \
|
||||
PHPMYADMIN_SOURCE_DIR="$TMP_DIR/phpMyAdmin-source" \
|
||||
PHPMYADMIN_INSTALL_ASSUME_ROOT=1 \
|
||||
CUSTOMBUILD_DIR="$CUSTOMBUILD_DIR" \
|
||||
bash "$SCRIPT" >/dev/null
|
||||
|
||||
PRIVATE_DIR="$TMP_DIR/alt-mysql-phpmyadmin"
|
||||
@@ -39,15 +37,27 @@ SIGNON_SCRIPT="$PRIVATE_DIR/da_signon.php"
|
||||
SIGNON_URL_PAGE="$PRIVATE_DIR/da_login.php"
|
||||
TICKET_DIR="$PRIVATE_DIR/runtime/tickets"
|
||||
SESSION_DIR="$TMP_DIR/sessions"
|
||||
ALIAS_CONF="$CUSTOMBUILD_DIR/custom/ap2/conf/extra/httpd-alias.conf"
|
||||
|
||||
[ -r "$PRIVATE_DIR/index.php" ] || fail "private phpMyAdmin copy was not installed"
|
||||
[ -r "$CONFIG_INC" ] || fail "private phpMyAdmin config was not created"
|
||||
[ -r "$SIGNON_SCRIPT" ] || fail "signon script was not created"
|
||||
[ -r "$SIGNON_URL_PAGE" ] || fail "phpMyAdmin SignonURL page was not created"
|
||||
[ -d "$TICKET_DIR" ] || fail "phpMyAdmin ticket directory was not created"
|
||||
if grep -Fq "config.mysql.inc.php" "$TMP_DIR/phpMyAdmin/config.inc.php"; then
|
||||
fail "installer modified existing CustomBuild phpMyAdmin config"
|
||||
fi
|
||||
|
||||
[ -r "$ALIAS_CONF" ] || fail "Apache alias config was not created"
|
||||
grep -Fq "# BEGIN ALT_MYSQL_PHPMYADMIN" "$ALIAS_CONF" || fail "Apache alias block marker missing"
|
||||
grep -Fq "Alias /alt-mysql-phpmyadmin ${PRIVATE_DIR}" "$ALIAS_CONF" || fail "Apache alias does not point at private phpMyAdmin dir"
|
||||
grep -Fq "# END ALT_MYSQL_PHPMYADMIN" "$ALIAS_CONF" || fail "Apache alias end marker missing"
|
||||
|
||||
PHPMYADMIN_PRIVATE_DIR="$TMP_DIR/alt-mysql-phpmyadmin" \
|
||||
PHPMYADMIN_SOURCE_DIR="$TMP_DIR/phpMyAdmin-source" \
|
||||
PHPMYADMIN_INSTALL_ASSUME_ROOT=1 \
|
||||
CUSTOMBUILD_DIR="$CUSTOMBUILD_DIR" \
|
||||
bash "$SCRIPT" >/dev/null
|
||||
|
||||
BEGIN_COUNT="$(grep -Fc "# BEGIN ALT_MYSQL_PHPMYADMIN" "$ALIAS_CONF" || true)"
|
||||
[ "$BEGIN_COUNT" -eq 1 ] || fail "Apache alias block was duplicated on re-run (found $BEGIN_COUNT times)"
|
||||
|
||||
SERVER_JSON="$(php -r '
|
||||
$_SERVER["HTTPS"] = "on";
|
||||
@@ -143,7 +153,16 @@ php -d session.save_path="$SESSION_DIR" -r '
|
||||
exit(3);
|
||||
}
|
||||
' "$SIGNON_SCRIPT" || fail "signon script cannot read SSO session while phpMyAdmin session is active"
|
||||
PHPMYADMIN_PRIVATE_DIR="$PRIVATE_DIR" bash "$PLUGIN_DIR/scripts/setup/phpmyadmin_health_check.sh" >/dev/null \
|
||||
|| fail "phpMyAdmin health check should pass for SignonURL-capable config"
|
||||
PHPMYADMIN_PRIVATE_DIR="$PRIVATE_DIR" CUSTOMBUILD_DIR="$CUSTOMBUILD_DIR" \
|
||||
bash "$PLUGIN_DIR/scripts/setup/phpmyadmin_health_check.sh" >/dev/null \
|
||||
|| fail "phpMyAdmin health check should pass for SignonURL-capable config with Apache alias present"
|
||||
|
||||
EMPTY_CUSTOMBUILD_DIR="$TMP_DIR/custombuild-empty"
|
||||
mkdir -p "$EMPTY_CUSTOMBUILD_DIR"
|
||||
HEALTH_OUTPUT="$(PHPMYADMIN_PRIVATE_DIR="$PRIVATE_DIR" CUSTOMBUILD_DIR="$EMPTY_CUSTOMBUILD_DIR" \
|
||||
bash "$PLUGIN_DIR/scripts/setup/phpmyadmin_health_check.sh" 2>&1)" \
|
||||
&& fail "phpMyAdmin health check should fail when the Apache alias is missing"
|
||||
echo "$HEALTH_OUTPUT" | grep -Fqi "alias" \
|
||||
|| fail "phpMyAdmin health check failure message should mention the missing Apache alias (got: $HEALTH_OUTPUT)"
|
||||
|
||||
echo "phpmyadmin_install_test: OK"
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$pluginDir = dirname(__DIR__);
|
||||
require_once $pluginDir . '/exec/lib/Settings.php';
|
||||
require_once $pluginDir . '/exec/lib/MySQLService.php';
|
||||
require_once $pluginDir . '/exec/lib/PhpMyAdminSso.php';
|
||||
|
||||
function fail(string $message): void
|
||||
{
|
||||
fwrite(STDERR, "FAIL: {$message}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function assert_true(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
fail($message);
|
||||
}
|
||||
}
|
||||
|
||||
function assert_same($expected, $actual, string $message): void
|
||||
{
|
||||
if ($expected !== $actual) {
|
||||
fail($message . ' expected=' . var_export($expected, true) . ' actual=' . var_export($actual, true));
|
||||
}
|
||||
}
|
||||
|
||||
function load_settings(string $contents): Settings
|
||||
{
|
||||
$path = tempnam(sys_get_temp_dir(), 'altmysql-settings-');
|
||||
if ($path === false) {
|
||||
fail('cannot create temp settings file');
|
||||
}
|
||||
file_put_contents($path, $contents);
|
||||
$settings = Settings::load($path);
|
||||
unlink($path);
|
||||
return $settings;
|
||||
}
|
||||
|
||||
// --- Temporary SSO role must be host-scoped, not wildcard '%' ---
|
||||
|
||||
$mysqlServiceSource = (string)file_get_contents($pluginDir . '/exec/lib/MySQLService.php');
|
||||
assert_true(
|
||||
!str_contains($mysqlServiceSource, "@\\'%\\'"),
|
||||
'MySQLService must not create/grant/drop temporary phpMyAdmin SSO roles on the wildcard host (%)'
|
||||
);
|
||||
|
||||
$mysqlServiceClass = new ReflectionClass(MySQLService::class);
|
||||
assert_true($mysqlServiceClass->hasConstant('TMP_ROLE_HOST'), 'MySQLService must define a TMP_ROLE_HOST constant');
|
||||
assert_same('127.0.0.1', $mysqlServiceClass->getConstant('TMP_ROLE_HOST'), 'TMP_ROLE_HOST must scope the temporary SSO role to the phpMyAdmin app host');
|
||||
|
||||
// --- phpMyAdmin login ticket files must not be world-readable ---
|
||||
|
||||
$ssoDir = sys_get_temp_dir() . '/altmysql-pma-sso-' . bin2hex(random_bytes(4));
|
||||
mkdir($ssoDir, 0755, true);
|
||||
|
||||
$settings = load_settings("PHPMYADMIN_SSO_DIR={$ssoDir}\nPHPMYADMIN_TICKET_TTL=60\n");
|
||||
|
||||
$ssoClass = new ReflectionClass(PhpMyAdminSso::class);
|
||||
/** @var PhpMyAdminSso $sso */
|
||||
$sso = $ssoClass->newInstanceWithoutConstructor();
|
||||
$settingsProp = $ssoClass->getProperty('settings');
|
||||
$settingsProp->setAccessible(true);
|
||||
$settingsProp->setValue($sso, $settings);
|
||||
|
||||
$writeTicket = $ssoClass->getMethod('writeLoginTicket');
|
||||
$writeTicket->setAccessible(true);
|
||||
$auth = [
|
||||
'user' => 'da_tmp_phpmyadmin_demo_test',
|
||||
'password' => 'secret-password',
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 33033,
|
||||
'db' => 'demo_db',
|
||||
];
|
||||
$token = $writeTicket->invoke($sso, $auth, 'demo_db');
|
||||
|
||||
assert_true(preg_match('/\A[a-f0-9]{64}\z/', $token) === 1, 'ticket token must be a 64 char lowercase hex string');
|
||||
|
||||
$ticketPath = $ssoDir . '/tickets/' . $token . '.json';
|
||||
assert_true(is_file($ticketPath), 'ticket file must be written to <sso_dir>/tickets/<token>.json');
|
||||
|
||||
$mode = fileperms($ticketPath) & 0777;
|
||||
assert_same(0640, $mode, 'ticket files contain a plaintext MySQL password and must not be world-readable (expected 0640)');
|
||||
|
||||
$decoded = json_decode((string)file_get_contents($ticketPath), true);
|
||||
assert_true(is_array($decoded), 'ticket file must contain valid JSON');
|
||||
assert_same('demo_db', $decoded['db'] ?? null, 'ticket must record the target database');
|
||||
assert_same('da_tmp_phpmyadmin_demo_test', $decoded['auth']['user'] ?? null, 'ticket must record the temporary role name');
|
||||
|
||||
// cleanup
|
||||
array_map('unlink', glob($ssoDir . '/tickets/*') ?: []);
|
||||
@rmdir($ssoDir . '/tickets');
|
||||
@rmdir($ssoDir);
|
||||
|
||||
echo "phpmyadmin_sso_test: OK\n";
|
||||
@@ -14,7 +14,7 @@ case "$(basename "$PLUGIN_DIR")" in
|
||||
esac
|
||||
grep -Fxq "name=alt-mysql" "$PLUGIN_DIR/plugin.conf" || fail "plugin.conf name is not alt-mysql"
|
||||
grep -Fxq "id=alt-mysql" "$PLUGIN_DIR/plugin.conf" || fail "plugin.conf id is not alt-mysql"
|
||||
grep -Fxq "version=1.2.11" "$PLUGIN_DIR/plugin.conf" || fail "plugin.conf version is not 1.2.11"
|
||||
grep -Fxq "version=1.2.12" "$PLUGIN_DIR/plugin.conf" || fail "plugin.conf version is not 1.2.12"
|
||||
grep -Fq "return '/CMD_PLUGINS/alt-mysql';" "$PLUGIN_DIR/exec/lib/AppContext.php" \
|
||||
|| fail "AppContext base URL is not alt-mysql"
|
||||
grep -Fq "\$pluginId = 'alt-mysql';" "$PLUGIN_DIR/exec/lib/DirectAdminUser.php" \
|
||||
|
||||
Reference in New Issue
Block a user