Forums Bug Reports Thread

Stored XSS in mobieusGate paywall body — strip_tags allowlist preserves event-handler attributes and javascript: URIs

Patrick Bass · Jun 6 · 15 · 1 Locked
[Major] [High Priority] [Bug Fixed] [Always Reproduces]
🚀 OP Jun 6, 2026 8:34pm

Area: Admin deep-dive (commerce/config) (audit p15b) · Surface: /admin/gate (savePaywallContent) → /gate paywall page (templates/gate/paywall.php) · Dimension: security · Severity: major

A Tenant Admin (role 4) can save arbitrary HTML in the paywall body. The save handler does not purify it, and the public paywall page renders it with strip_tags() which is well known to be insufficient against XSS because it preserves attributes on the allowed tags (onerror/onload/onmouseover handlers and javascript: hrefs). The paywall page is shown to every gated visitor, so this is a privilege-boundary crossing: a role-4 admin executes arbitrary JavaScript in the browser of every visitor including the role-5 Tenant Super Admin, enabling session/cookie theft and CSRF-token exfiltration. OWASP A03:2021 (Injection / Cross-Site Scripting).

Evidence

AdminGateController::savePaywallContent stores the body verbatim with no HTML sanitization: platform/src/Controllers/AdminGateController.php:251 `$body = trim($_POST['paywall_body'] ?? '');` then GateConfig::update(['paywall_body' => $body !== '' ? $body : null]) (line 253-256). The public paywall template then renders it through strip_tags only: platform/templates/gate/paywall.php:39-48 `$allowedTags = '<p><br><strong><em><a><img><ul><ol><li><h2><h3><h4><div><span><hr>'; $sanitizedBody = strip_tags($bodyHtml, $allowedTags); ... <div class="gt-paywall__body"><?= $sanitizedBody ?></div>`. strip_tags keeps the allowed tags but strips NONE of their attributes, so `<img src=x onerror=alert(document.cookie)>`, `<a href="javascript:...">`, or `<div onmouseover=...>` all survive and execute. The in-template comment even acknowledges 'inline styles flow through.'

Suggested fix. Run paywall_body through HTMLPurifier (App\Services\MarkdownService::purifyLongForm()) either on save in savePaywallContent() or at render in paywall.php, instead of strip_tags(). HTMLPurifier strips dangerous attributes and javascript: URIs while keeping the same semantic tag set.

Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.


Patrick Bass
@mobieus

🚀 Jun 7, 2026 5:25am

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

savePaywallContent() now sanitizes paywall_body via App\Services\MarkdownService::purifyLongForm() before persisting (added the use import), so stored HTML rendered by templates/gate/paywall.php is XSS-safe instead of stored raw. Heading is still plain-trimmed (it is escaped at render). php -l passes.

Status: fixed. Thread closed and locked.


Patrick Bass
@mobieus

Log in or register to reply to this thread.