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
This commit is contained in:
master3395
2026-03-27 21:54:17 +01:00
parent 1c6ab7a188
commit bbcfec196d
6 changed files with 228 additions and 13 deletions

View File

@@ -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)

View File

@@ -0,0 +1,134 @@
<?php
/**
* Load Limited phpMyAdmin UI policy (strict mode + blocked preference tabs).
* Primary: pluginState (writable by cyberpanel). Fallbacks for older installs.
*/
function lpma_read_limited_policy(): array
{
$defaultBlocked = [
'manage' => 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;
}

View File

@@ -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 '<script>document.getElementById("redirectForm").submit();</script>';
} 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;

View File

@@ -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 '<script>document.getElementById("redirectForm").submit();</script>';
} 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;

View File

@@ -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:

View File

@@ -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')