GHSA-hpgw-ww76-c68r: phpMyFAQ has an Authorization Bypass in All Admin Pages Due to Non-Terminating Permission Check
Summary`AbstractAdministrationController::userHasPermission()` catches the `ForbiddenException` thrown when a user lacks a specific permission, sends a "forbidden" HTML page via `$response->send()`, but does not terminate execution. The calling controller method continues to execute, fetches protected data, renders the full template, and returns it as a Response. The final `$response->send()` in `admin/index.php` outputs the protected page content after the forbidden page, leaking all permission-protected admin data to any authenticated admin user regardless of their actual permissions.DetailsThe parent class `AbstractController::userHasPermission()` (`phpmyfaq/src/phpMyFAQ/Controller/AbstractController.php:317-327`) correctly enforces authorization by throwing a `ForbiddenException` when the user lacks the required permission. This exception would normally propagate to Symfony's HttpKernel exception handler, which would return an error response and prevent the controller from continuing.However, `AbstractAdministrationController` overrides this method at line 390-399:The critical flaw: after `$response->send()` at line 396, there is no `exit()`, `die()`, `return`, or re-throw. PHP execution continues normally into the calling controller method.For example, in `AdminLogController::index()` (`phpmyfaq/src/phpMyFAQ/Controller/Administration/AdminLogController.php:45-83`):The entry point `admin/index.php` then calls `$response->send()` on the returned Response, appending the full protected page to the already-sent forbidden page in the HTTP response body.The second `catch` block (line 397-398) for generic `Exception` is even worse — it only logs the error without sending any response or terminating, so the protected page renders with no forbidden notice at all.**58 admin controllers** extend `AbstractAdministrationController` and call `userHasPermission()`, meaning every permission-protected admin page is affected. This includes:Admin logs (user IPs, actions, usernames)User management (user data, permissions)System information (server configuration, PHP info)Configuration pages (all application settings)Backup pagesAll other admin functionalityPoCCreate a test admin user with minimal permissions (e.g., only FAQ editing, no statistics access):Authenticate as the limited admin user and request a permission-protected page:The HTTP response body contains the forbidden page HTML followed by the full protected page HTML, including all sensitive data.ImpactAny authenticated admin user — even one with zero administrative permissions beyond basic login — can access **every** permission-protected admin page by simply requesting its URL. The permission check sends a forbidden page but does not stop execution, so the protected content is always appended to the response.Exposed data includes:**Admin logs**: All admin users' IP addresses, actions, and timestamps**User management**: User accounts, email addresses, permissions**System information**: PHP configuration, database details, server paths**Configuration**: All application settings including security-sensitive values**Backups**: Database export functionalityThis effectively renders the entire admin permission system non-functional for the 58 page controllers using `AbstractAdministrationController`.Recommended FixAdd `return` after sending the forbidden response, and re-throw for the generic Exception case:A cleaner architectural fix would be to not swallow the exception at all, and instead let it propagate to the Symfony HttpKernel exception handler (which already handles `ForbiddenException` via `WebExceptionListener`):Or remove the override entirely, since the `WebExceptionListener` registered in the Kernel already handles exception-to-response conversion.
CVSS v3.1: 6.5