CVE-2026-75828: Grav: detectXss() misses an event-handler attribute after an unpaired quote in an unquoted attribute value, giving stored XSS
Affected versions and vulnerable location - Confirmed on grav core at 78ebfc1 (tag 2.0.13). - Detector: system/src/Grav/Common/Security.php:290, the onevents regex, run via patternMatches() (:315-330). - The onevents pattern at HEAD: # "'])?(?:[\s\x00-\x20"'/]"[^"]"'[^']')on\s[a-z]+\s=#iu - Sole save-time guard for non-super content: Validation::checkSafety() (system/src/Grav/Common/Data/Validation.php:160 scalars,…
Published Sep 17, 2026
Affected packages
getgrav/grav
- Ecosystem
- Composer (PHP)
- Affected
- <= 2.0.14
- Fixed in
- 2.0.15
- Check
composer show getgrav/grav
Advisory details
From the GitHub Advisory Database (CC BY 4.0).
Affected versions and vulnerable location
- Confirmed on grav core at
78ebfc1(tag 2.0.13). - Detector:
system/src/Grav/Common/Security.php:290, theon_eventsregex, run viapatternMatches()(:315-330). - The
on_eventspattern at HEAD:#<(?:"[^"]*"|'[^']*'|[^>"'])*?(?:[\s\x00-\x20"'/]|"[^"]*"|'[^']*')on\s*[a-z]+\s*=#iu - Sole save-time guard for non-super content:
Validation::checkSafety()(system/src/Grav/Common/Data/Validation.php:160scalars,:165arrays), invoked per field fromBlueprintSchema::validate->Validation::checkSafety(system/src/Grav/Common/Data/BlueprintSchema.php:248).security.xss_whitelist: [admin.super]exempts only super-admins (Validation.php:148).
Root cause (distinct from GHSA-269c)
GHSA-269c hardened the tag-body scan to be quote-aware so a > inside a paired quoted attribute value is treated as data, not a tag close. That same quote-awareness opened a new gap: the regex treats ANY " or ' as a string delimiter, but HTML only enters a quoted-value state when a quote appears immediately after =. A single unpaired quote sitting inside an unquoted attribute value is, to the browser, just a value character; to the regex it is an unterminated string that neither [^>"'] nor "[^"]*" can consume, so the lazy tag-body scan cannot advance past it to reach the following on...= handler. No alignment matches and detectXss() returns null.
Proof (executed)
The detector was replicated verbatim (the on_events regex plus patternMatches) with the shipped system/config/security.yaml defaults and run under PHP. Observed:
baseline => blocked (on_events)
GHSA-269c " onerror=alert(1)> => blocked (on_events) # prior fix works
BYPASS A => PASSES (no XSS detected)
BYPASS B => PASSES (no XSS detected)
BYPASS C x => PASSES (no XSS detected)
BYPASS D => PASSES (no XSS detected)
BYPASS E hover => PASSES (no XSS detected)Browser tokenization of `: src takes the unquoted value x" (space ends it), onerror is parsed as a separate live attribute, src 404s and onerror fires. None of the other rules cover it: img/a/div are not in xss_dangerous_tags, there is no javascript:/data: scheme and no style/url/expression`.
Reachability
checkSafety() is the only save-time XSS screen for a non-super editor. The pages blueprint validates header.title (type: text) and page content (markdown/textarea) with xss_check on, so a bare onerror= is rejected but the payload above is stored verbatim. Page content is emitted through {{ page.content|raw }} and raw inline HTML passes Parsedown by default (markdown.escape_markup: false), so the handler runs for every visitor. The same detector core also backs Security::detectXssInEditorContent() (the GHSA-2c4f render-time-Twig save gate; callers Page.php:1359, Flex/Types/Pages/PageObject.php:194), the detectXssFromPages() admin scanner (Admin.php:2096), and the xss() Twig function, so all of them report the payload clean.
Auth required: an authenticated content editor with page/form edit rights but WITHOUT admin.super.
Suggested fix
Make the tag-body scan treat a quote as a delimiter only in the after-= position, or normalize unquoted attribute values before the handler scan, so an unpaired quote inside an unquoted value cannot mask a following on...=. A targeted addition: also flag on<name>= sequences that appear after a lone unbalanced quote within the same tag. Because detectXss() is a denylist, consider additionally encoding "/' in stored non-super content, or defaulting markdown.escape_markup: true for non-super authors.
Severity and CVSS reasoning
Suggested severity: High (matches GHSA-269c and the other stored-XSS advisories in this codebase).
Suggested CVSS:3.1 vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N (8.7).
PR:L: a non-super editor account.UI:R: a visitor renders the page (foronerrorthe image simply loads/fails automatically).S:C/C:H/I:H: script in the site origin against every visitor, including admins viewing the content.
How I found it and a note on tooling
I read detectXss() and reasoned about the difference between the HTML tokenizer's quoted-value state and the regex's string handling, then replicated the exact on_events pattern and patternMatches() with the shipped default config and ran the payloads to confirm the bypass and that the GHSA-269c payload is still blocked. I used AI assistance for the analysis and drafting and verified the detector behavior by execution. This is executed against a faithful replica of the detector with production config, not against a full running Grav site.
Credited to arpitjain099.
References
How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. No analysis has been written for this entry yet. Methodology
More Composer (PHP) vulnerabilities
All Composer (PHP) entriesCVE-2026-75827
Grav: Blueprint dynamic-data bare-function branch is denylist-gated and omits error_log, giving arbitrary file write
critical8.8Sep 17, 2026CVE-2026-42607
Grav Vulnerable to Remote Code Execution (RCE) via Malicious Plugin ZIP Upload in Direct Install Feature
critical9.1May 5, 2026CVE-2026-42613
Grav Vulnerable to Privilege Escalation via Missing Server-Side Validation of groups/access
critical9.4May 5, 2026GHSA-vj3m-2g9h-vm4p
Grav has multiple RCE vectors: unsafe unserialize (x3), command injection in git clone, SSTI blocklist bypass
criticalMay 5, 2026CVE-2025-66844
Grav may be vulnerable to SSRF attack via Twig Templates
critical9.1No fixDec 15, 2025
