From 2c1f8f893303b81536cca655f9eb3152339dc569 Mon Sep 17 00:00:00 2001 From: master3395 Date: Fri, 27 Mar 2026 21:54:17 +0100 Subject: [PATCH] fix: LPMA launch routes in secMiddleware; strict sign-on + lpma_policy_read - secMiddleware: allow Limited phpMyAdmin launch URLs and phpmyadminsignin without JSON-body filtering that breaks sign-on POSTs - plogical/public phpmyadminsignin: lpma_policy_read.inc.php, strict cookie helpers - webmail: section header comments only --- CyberCP/secMiddleware.py | 14 ++- plogical/lpma_policy_read.inc.php | 134 +++++++++++++++++++++++++ plogical/phpmyadminsignin.php | 24 +++++ public/phpmyadmin/phpmyadminsignin.php | 35 +++++++ webmail/services/imap_client.py | 4 +- webmail/views.py | 30 ++++-- 6 files changed, 228 insertions(+), 13 deletions(-) create mode 100644 plogical/lpma_policy_read.inc.php diff --git a/CyberCP/secMiddleware.py b/CyberCP/secMiddleware.py index 3f30e0941..8d58422f6 100644 --- a/CyberCP/secMiddleware.py +++ b/CyberCP/secMiddleware.py @@ -38,8 +38,13 @@ class secMiddleware: import re webhook_pattern = re.compile(r'^/websites/[^/]+/(webhook|gitNotify)/?$') - if pathActual == "/backup/localInitiate" or pathActual == '/' or pathActual == '/verifyLogin' or pathActual == '/logout' or pathActual.startswith('/api')\ - or webhook_pattern.match(pathActual) or pathActual.startswith('/cloudAPI') or pathActual.startswith('/static/'): + # Public one-time phpMyAdmin launch links (limitedPhpmyAdmin plugin); must work when admin is logged out. + _lpma_public_launch = pathActual.startswith('/plugins/limitedPhpmyAdmin/launch/') + _lpma_pma_signon = pathActual == '/phpmyadmin/phpmyadminsignin.php' + + if pathActual == "/backup/localInitiate" or pathActual == '/' or pathActual == '/verifyLogin' or pathActual == '/logout' or pathActual.startswith('/api')\ + or webhook_pattern.match(pathActual) or pathActual.startswith('/cloudAPI') or pathActual.startswith('/static/')\ + or _lpma_public_launch or _lpma_pma_signon: pass else: # Session check logging removed @@ -108,6 +113,11 @@ class secMiddleware: response = self.get_response(request) return response + # phpMyAdmin sign-on POST carries MySQL password; skip character filter (may contain $ ( ) etc.). + if pathActual == '/phpmyadmin/phpmyadminsignin.php': + response = self.get_response(request) + return response + # logging.writeToFile(request.body) try: data = json.loads(request.body) diff --git a/plogical/lpma_policy_read.inc.php b/plogical/lpma_policy_read.inc.php new file mode 100644 index 000000000..2281ab389 --- /dev/null +++ b/plogical/lpma_policy_read.inc.php @@ -0,0 +1,134 @@ + true, + 'two_factor' => true, + 'features' => true, + 'sql' => true, + 'navigation' => true, + 'main_panel' => true, + 'export' => true, + 'import' => true, + ]; + $policy = [ + 'strict_mode' => true, + 'blocked_tabs' => $defaultBlocked, + ]; + $paths = [ + '/usr/local/CyberCP/pluginState/limited_phpmyadmin_policy.json', + '/var/lib/cyberpanel-panelstate/limited_phpmyadmin_policy.json', + '/etc/cyberpanel/limited_phpmyadmin_policy.json', + ]; + foreach ($paths as $policyPath) { + if (! @is_readable($policyPath)) { + continue; + } + $raw = @file_get_contents($policyPath); + if ($raw === false) { + continue; + } + $decoded = @json_decode($raw, true); + if (! is_array($decoded)) { + continue; + } + $policy['strict_mode'] = isset($decoded['strict_mode']) ? (bool) $decoded['strict_mode'] : true; + if (isset($decoded['blocked_tabs']) && is_array($decoded['blocked_tabs'])) { + foreach ($defaultBlocked as $k => $_v) { + $policy['blocked_tabs'][$k] = isset($decoded['blocked_tabs'][$k]) + ? (bool) $decoded['blocked_tabs'][$k] + : true; + } + } + break; + } + + return $policy; +} + +/** + * True if a cpma_* request to this application route must be turned away (Settings prefs + main menu targets). + * Does not block table browse at route "/sql" (that is Browse, not the SQL runner). + */ +function lpma_cpma_route_blocked(string $requestedRoute, array $policy): bool +{ + if ($requestedRoute === '') { + return false; + } + $bt = $policy['blocked_tabs'] ?? []; + $blocked = static function (string $k) use ($bt): bool { + return (($bt[$k] ?? true) === true); + }; + + if (strpos($requestedRoute, '/preferences') === 0) { + $routeToTab = [ + '/preferences/manage' => 'manage', + '/preferences/two-factor' => 'two_factor', + '/preferences/features' => 'features', + '/preferences/sql' => 'sql', + '/preferences/navigation' => 'navigation', + '/preferences/main-panel' => 'main_panel', + '/preferences/export' => 'export', + '/preferences/import' => 'import', + ]; + if (isset($routeToTab[$requestedRoute])) { + return $blocked($routeToTab[$requestedRoute]); + } + + return (($policy['strict_mode'] ?? true) === true); + } + + if ($blocked('sql')) { + if (preg_match('#^/(server|database|table)/sql$#', $requestedRoute) === 1) { + return true; + } + if ($requestedRoute === '/database/multi-table-query' || $requestedRoute === '/database/qbe') { + return true; + } + } + + if ($blocked('export') && preg_match('#^/(server|database|table)/export$#', $requestedRoute) === 1) { + return true; + } + + if ($blocked('import') && preg_match('#^/(server|database|table)/import$#', $requestedRoute) === 1) { + return true; + } + + if ($blocked('main_panel')) { + if ( + $requestedRoute === '/server/databases' + || $requestedRoute === '/server/variables' + || $requestedRoute === '/server/collations' + ) { + return true; + } + if (strpos($requestedRoute, '/server/status') === 0) { + return true; + } + } + + if ($blocked('features')) { + if ( + $requestedRoute === '/server/engines' + || $requestedRoute === '/server/plugins' + || $requestedRoute === '/server/binlog' + ) { + return true; + } + if ( + $requestedRoute === '/database/designer' + || $requestedRoute === '/database/central-columns' + || $requestedRoute === '/database/tracking' + || $requestedRoute === '/table/tracking' + ) { + return true; + } + } + + return false; +} diff --git a/plogical/phpmyadminsignin.php b/plogical/phpmyadminsignin.php index 1ac2461e9..051bfd595 100644 --- a/plogical/phpmyadminsignin.php +++ b/plogical/phpmyadminsignin.php @@ -3,10 +3,28 @@ define("PMA_SIGNON_INDEX", 1); +require_once __DIR__ . '/lpma_policy_read.inc.php'; + try { define('PMA_SIGNON_SESSIONNAME', 'SignonSession'); define('PMA_DISABLE_SSL_PEER_VALIDATION', TRUE); + function lpma_set_strict_cookie($enabled) { + $opts = array( + 'expires' => $enabled ? (time() + 86400) : (time() - 86400), + 'path' => '/phpmyadmin/', + 'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', + 'httponly' => true, + 'samesite' => 'Lax', + ); + setcookie('PMA_LPMA_STRICT', $enabled ? '1' : '', $opts); + } + + function lpma_global_strict_mode_enabled() { + $p = lpma_read_limited_policy(); + return ! empty($p['strict_mode']); + } + // Handle both GET and POST parameters for token and username $token = isset($_POST['token']) ? $_POST['token'] : (isset($_GET['token']) ? $_GET['token'] : null); $username = isset($_POST['username']) ? $_POST['username'] : (isset($_GET['username']) ? $_GET['username'] : null); @@ -32,6 +50,7 @@ try { echo ''; } else if (isset($_POST['logout']) || isset($_GET['logout'])) { + lpma_set_strict_cookie(false); session_name(PMA_SIGNON_SESSIONNAME); @session_start(); $_SESSION = array(); @@ -47,9 +66,14 @@ try { $username = htmlspecialchars($_POST['username'], ENT_QUOTES, 'UTF-8'); $password = $_POST['password']; + $strictMode = (isset($_POST['lpma_strict']) && $_POST['lpma_strict'] === '1'); + $isLimitedUser = (strpos($username, 'cpma_') === 0); $host = isset($_POST['host']) ? trim($_POST['host']) : '127.0.0.1'; if ($host === 'localhost') { $host = '127.0.0.1'; } + $effectiveStrictMode = ($strictMode || lpma_global_strict_mode_enabled()) && $isLimitedUser; + lpma_set_strict_cookie($effectiveStrictMode); + $_SESSION['PMA_single_signon_user'] = $username; $_SESSION['PMA_single_signon_password'] = $password; $_SESSION['PMA_single_signon_host'] = $host; diff --git a/public/phpmyadmin/phpmyadminsignin.php b/public/phpmyadmin/phpmyadminsignin.php index 1ac2461e9..4a2a04b2f 100644 --- a/public/phpmyadmin/phpmyadminsignin.php +++ b/public/phpmyadmin/phpmyadminsignin.php @@ -3,10 +3,39 @@ define("PMA_SIGNON_INDEX", 1); +// Policy helper ships in plogical/ (same layout as phpmyadmin index.php) +$_lpma_policy = dirname(dirname(__DIR__)) . '/plogical/lpma_policy_read.inc.php'; +if (is_readable($_lpma_policy)) { + require_once $_lpma_policy; +} elseif (is_readable(__DIR__ . '/lpma_policy_read.inc.php')) { + require_once __DIR__ . '/lpma_policy_read.inc.php'; +} else { + http_response_code(500); + header('Content-Type: text/plain; charset=utf-8'); + echo 'phpMyAdmin sign-on is misconfigured: lpma_policy_read.inc.php is missing.'; + exit; +} + try { define('PMA_SIGNON_SESSIONNAME', 'SignonSession'); define('PMA_DISABLE_SSL_PEER_VALIDATION', TRUE); + function lpma_set_strict_cookie($enabled) { + $opts = array( + 'expires' => $enabled ? (time() + 86400) : (time() - 86400), + 'path' => '/phpmyadmin/', + 'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', + 'httponly' => true, + 'samesite' => 'Lax', + ); + setcookie('PMA_LPMA_STRICT', $enabled ? '1' : '', $opts); + } + + function lpma_global_strict_mode_enabled() { + $p = lpma_read_limited_policy(); + return ! empty($p['strict_mode']); + } + // Handle both GET and POST parameters for token and username $token = isset($_POST['token']) ? $_POST['token'] : (isset($_GET['token']) ? $_GET['token'] : null); $username = isset($_POST['username']) ? $_POST['username'] : (isset($_GET['username']) ? $_GET['username'] : null); @@ -32,6 +61,7 @@ try { echo ''; } else if (isset($_POST['logout']) || isset($_GET['logout'])) { + lpma_set_strict_cookie(false); session_name(PMA_SIGNON_SESSIONNAME); @session_start(); $_SESSION = array(); @@ -47,9 +77,14 @@ try { $username = htmlspecialchars($_POST['username'], ENT_QUOTES, 'UTF-8'); $password = $_POST['password']; + $strictMode = (isset($_POST['lpma_strict']) && $_POST['lpma_strict'] === '1'); + $isLimitedUser = (strpos($username, 'cpma_') === 0); $host = isset($_POST['host']) ? trim($_POST['host']) : '127.0.0.1'; if ($host === 'localhost') { $host = '127.0.0.1'; } + $effectiveStrictMode = ($strictMode || lpma_global_strict_mode_enabled()) && $isLimitedUser; + lpma_set_strict_cookie($effectiveStrictMode); + $_SESSION['PMA_single_signon_user'] = $username; $_SESSION['PMA_single_signon_password'] = $password; $_SESSION['PMA_single_signon_host'] = $host; diff --git a/webmail/services/imap_client.py b/webmail/services/imap_client.py index 5bd36e043..66c1fdfea 100644 --- a/webmail/services/imap_client.py +++ b/webmail/services/imap_client.py @@ -98,7 +98,9 @@ class IMAPClient: def _folder_type(self, folder_name): """Identify special folder type for UI (icons, sidebar grouping). - CyberPanel/Dovecot uses INBOX.* names; accounts may also use Spam, Trash, etc. + CyberPanel/Dovecot uses INBOX.* names, but some accounts also have + INBOX.spam, Trash, Archive, etc. Classify those so they are not treated + as generic user folders. """ fn = (folder_name or '').strip() if not fn: diff --git a/webmail/views.py b/webmail/views.py index 4629fee1d..638534da2 100644 --- a/webmail/views.py +++ b/webmail/views.py @@ -5,7 +5,8 @@ from loginSystem.views import loadLoginPage from .webmailManager import WebmailManager -# --- Page Views --- +# ── Page Views ──────────────────────────────────────────────── + def loadWebmail(request): try: wm = WebmailManager(request) @@ -23,7 +24,8 @@ def loadLogin(request): return wm.loadLogin() -# --- Auth APIs --- +# ── Auth APIs ───────────────────────────────────────────────── + def apiLogin(request): try: wm = WebmailManager(request) @@ -70,7 +72,8 @@ def apiSwitchAccount(request): return _error_response(e) -# --- Folder APIs --- +# ── Folder APIs ─────────────────────────────────────────────── + def apiListFolders(request): try: wm = WebmailManager(request) @@ -111,7 +114,8 @@ def apiDeleteFolder(request): return _error_response(e) -# --- Message APIs --- +# ── Message APIs ────────────────────────────────────────────── + def apiListMessages(request): try: wm = WebmailManager(request) @@ -152,7 +156,8 @@ def apiGetAttachment(request): return _error_response(e) -# --- Action APIs --- +# ── Action APIs ─────────────────────────────────────────────── + def apiSendMessage(request): try: wm = WebmailManager(request) @@ -223,7 +228,8 @@ def apiMarkFlagged(request): return _error_response(e) -# --- Contact APIs --- +# ── Contact APIs ────────────────────────────────────────────── + def apiListContacts(request): try: wm = WebmailManager(request) @@ -323,7 +329,8 @@ def apiImportRulesFromSnappymail(request): return _error_response(e) -# --- Sieve Rule APIs --- +# ── Sieve Rule APIs ────────────────────────────────────────── + def apiListRules(request): try: wm = WebmailManager(request) @@ -374,7 +381,8 @@ def apiActivateRules(request): return _error_response(e) -# --- Settings APIs --- +# ── Settings APIs ───────────────────────────────────────────── + def apiGetSettings(request): try: wm = WebmailManager(request) @@ -395,7 +403,8 @@ def apiSaveSettings(request): return _error_response(e) -# --- Image Proxy --- +# ── Image Proxy ─────────────────────────────────────────────── + def apiProxyImage(request): try: wm = WebmailManager(request) @@ -404,7 +413,8 @@ def apiProxyImage(request): return _error_response(e) -# --- Helpers --- +# ── Helpers ─────────────────────────────────────────────────── + def _error_response(e): data = {'status': 0, 'error_message': str(e)} return HttpResponse(json.dumps(data), content_type='application/json')