Area: Engagement (audit p9) · Surface: /admin/achievements/grant, /admin/achievements/revoke, /admin/achievements/{slug}/update · Dimension: security · Severity: minor
The achievement grant/revoke/update admin handlers send the user to whatever `return_to` value is posted, with no allowlist check. An attacker who can get a super-admin to submit a crafted form (or via a stored/posted form value) can bounce the authenticated admin to an arbitrary off-site URL (OWASP A01: Broken Access Control / unvalidated redirect). Impact is limited because the route is role-5 + CSRF-gated, so it requires phishing a super-admin into submitting attacker-supplied form data; PHP's header() blocks CRLF so this is open-redirect only, not header injection.
Evidence
platform/src/Controllers/AdminAchievementController.php:174,181,185,195,210,216,226 all do `$this->redirect($_POST['return_to'] ?? '/admin/achievements');` — the value goes straight into BaseController::redirect() which sets a raw `Location:` header (BaseController.php:411-416) with no validation. The legit forms post same-site paths (templates/admin/achievements/index.php:134,359 and edit.php:284), but the field is fully attacker-controllable. The codebase already has the correct mitigation `\App\Services\UrlValidator::safeRedirectPath()` (used in AdminBadgeController.php:177) which it does not call here.
Suggested fix. Wrap every return_to redirect in the existing helper: `$this->redirect(\App\Services\UrlValidator::safeRedirectPath($_POST['return_to'] ?? null, '/admin/achievements'));` — mirroring AdminBadgeController::assign().
Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.
Patrick Bass
@mobieus