You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(admin-cabinet): handle 404s cleanly, plug menu info leak, polish error UX (#1002)
Three related changes driven by Sentry issue #1002
("Action 'start' was not found on handler 'Session'", 148 events, 4 users).
1. Attach NotFoundPlugin unconditionally.
DispatcherProvider gated NotFoundPlugin behind
class_exists(Whoops\Handler\PrettyPageHandler::class), but filp/whoops
is in require (not require-dev), so the class always exists and the
plugin was never attached in production. Every non-existent route
propagated as an unhandled dispatcher exception to Whoops and Sentry.
NotFoundPlugin itself is also narrowed: it now only forwards
EXCEPTION_HANDLER_NOT_FOUND / EXCEPTION_ACTION_NOT_FOUND to
errors/show404 and lets every other exception propagate, so real
bugs still reach WhoopsErrorHandlerProvider and Sentry.
2. Close information-disclosure via left sidebar.
Once NotFoundPlugin started working, the error page began rendering
via the main admin layout, whose partials/leftsidebar iterates
Elements::getMenu(). The ACL check in SecurityPlugin::isAllowedAction
fell back to ROLE_ADMINS when no JWT/refreshToken was present
("backward compatibility"), so anonymous callers were treated as
admin and the full module list was rendered into the HTML —
revealing installed modules, AMI/ARI feature flags, and admin URLs.
- isAllowedAction(): secure-by-default. If role is null, fall back
to ROLE_ADMINS only for isLocalHostRequest() (internal workers
and health checks), otherwise ROLE_GUESTS which the ACL denies.
Return value now uses (bool) cast — Phalcon 5 isAllowed() returns
bool, not AclEnum int, which would silently fail strict comparison.
- Elements::getMenu(): defense-in-depth early return when the caller
is not authenticated. The menu structure is never iterated and no
module information is emitted for anonymous requests.
3. Polished, self-contained 404/401/500 pages.
BaseController now gives ErrorsController a dedicated "error"
layout and sets LEVEL_AFTER_TEMPLATE so Views/index.volt is NOT
rendered. That matters because index.volt loads advice-worker,
user-page-tracker, connection-check-worker and pbx-api-client, and
those fire REST calls on DOM ready. On a 404 the calls can race
TokenManager warmup, return 401, and PbxApiClient.handleAuthError
redirects to /session/index — causing a redirect loop on the error
page for authenticated users.
layouts/error.volt is a self-contained HTML document with inline
style and direct Semantic UI base CSS links. It shows the MikoPBX
logo, the show40x body, and a branded "Go home" button whose target
switches between /extensions/index (authenticated) and /session/index
(anonymous). show404/show401/show500 views were shrunk accordingly.
ErrorsController now also sets the correct HTTP status code
(404/401/500) — previously the body said "not found" but the
response code was 200, which is exactly the kind of thing that
confused Sentry volume tracking for #1002.
BaseController additionally skips the "Breadcrumb{Controller}{Action}"
title assembly for ErrorsController so the title set in
ErrorsController::initialize ("Oops!") is preserved instead of
being overwritten with a non-existent translation key
(e.g. BreadcrumbErrorsshow404).
4. Narrow SecurityPlugin stale-cookie branch.
SecurityPlugin::beforeDispatch had a pre-existing branch that
clearAuthCookies() for any authenticated visit to any SessionController
action except "end". With the NotFoundPlugin fix above, visiting
/session/start (or any unknown session action) started reaching
that branch and silently logging authenticated users out before the
error page rendered. Narrowed to only trigger on the legitimate
login page (action === 'index'). SessionController only exposes
indexAction and endAction, so no other flows are affected.
5. Drop debug syslog noise from SecurityPlugin.
checkUserAuth() and extractRoleFromJwt() emitted LOG_DEBUG messages
on every request including the request URI, which is both a hot-path
cost and a modest log-leak. Removed.
Verified end-to-end in a real browser (Playwright) against production
and local Docker, including: anonymous 404, protected-controller
login redirect, real admin login, authenticated 404 in a regular
controller, authenticated visit to /session/start, and a follow-up
check that the user remained authenticated with a 75-item sidebar.
0 commit comments