Area: Admin plane (audit p12) · Surface: POST /admin/achievements/grant, POST /admin/achievements/revoke (AdminAchievementController) · Dimension: security · Severity: minor
A crafted form/POST with `return_to=//evil.example` (or `https://evil.example`) sent to /admin/achievements/grant or /revoke causes a 302 Location to the attacker-controlled host. OWASP A01:2021 Broken Access Control (open redirect). Severity is capped because the route requires role 5 + a valid CSRF token, so an attacker must already control a super-admin session/form — primarily a consistency/defense-in-depth defect rather than a remote pre-auth redirect. The fix is one line and the project clearly intends every return-to redirect to go through the allowlist (every other controller does).
Evidence
AdminAchievementController.php uses `$this->redirect($_POST['return_to'] ?? '/admin/achievements');` unfiltered at lines 174, 181, 185, 195, 210, 216, 226. BaseController::redirect() (platform/src/Controllers/BaseController.php:411-416) sends the value straight into `header("Location: {$url}")` with no validation. Sibling controllers performing the identical return-to pattern DO validate: AdminBadgeController.php:177 `$returnUrl = \App\Services\UrlValidator::safeRedirectPath($_SERVER['HTTP_REFERER'] ?? null, '/admin/badges');`, AdminFileController.php:531 same, AdminUserActionController.php:390-394 same. The platform even ships the helper for this: BaseController::isSafeReturnPath() (BaseController.php:717-732) and Services/UrlValidator::safeRedirectPath() (Services/UrlValidator.php:162). AdminAchievementController is the lone outlier that skips it.
Suggested fix. Replace each `$this->redirect($_POST['return_to'] ?? '/admin/achievements')` with `$this->redirect(\App\Services\UrlValidator::safeRedirectPath($_POST['return_to'] ?? null, '/admin/achievements'))`, matching AdminBadgeController/AdminFileController/AdminUserActionController.
Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.
Patrick Bass
@mobieus