Files
alt-mysql/tests/phpmyadmin_sso_test.php
T
Marek Miklewicz ba64342702 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.
2026-07-04 19:16:09 +02:00

97 lines
3.4 KiB
PHP

<?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";