Listen to this Post
The vulnerability exists in the `Security::detectXss()` function of Grav CMS, specifically in the `on_events` regex used to detect event-handler attributes in saved content. The regex was hardened in GHSA-269c to treat a `>` inside a quoted attribute value as data rather than a tag close, making the scan quote-aware. However, this same quote-awareness introduced a new bypass: the regex treats any `”` or `’` as a string delimiter, while the HTML tokenizer only enters a quoted-value state when a quote appears immediately after =. An unpaired quote inside an unquoted attribute value is, to the browser, just a value character, but to the regex it is an unterminated string that neither `[^>”‘]` nor `”[^”]”` can consume. The lazy tag-body scan therefore cannot advance past the unpaired quote to reach a following `on…=` handler. No alignment matches occur and `detectXss()` returns null, allowing the payload to be stored verbatim. When page content is rendered through `{{ page.content|raw }}` and raw inline HTML passes Parsedown by default (markdown.escape_markup: false), the event handler executes in the site origin for every visitor. An authenticated content editor with page/form edit rights but without `admin.super` can exploit this. The same detector core also backs Security::detectXssInEditorContent(), the `detectXssFromPages()` admin scanner, and the `xss()` Twig function, so all of them report the payload clean. The flaw affects Grav versions before 2.0.15 and is distinct from GHSA-269c, which addressed a different quote-parsing discrepancy.
DailyCVE Form:
Platform: Grav CMS
Version: 2.0.13
Vulnerability: Stored XSS
Severity: High
date: 2026-08-03
Prediction: 2026-09-17
What Undercode Say
Vulnerable detector core system/src/Grav/Common/Security.php:290 Regex pattern <(?:"[^"]"|'[^']'|[^>"'])?(?:[\s\x00-\x20"'/]|"[^"]"|'[^']')on\s[a-z]+\s=iu Run via patternMatches() at :315-330
// Sole save-time guard for non-super content Validation::checkSafety() // system/src/Grav/Common/Data/Validation.php:160 scalars, :165 arrays // Invoked per field from BlueprintSchema::validate -> Validation::checkSafety // system/src/Grav/Common/Data/BlueprintSchema.php:248 // xss_whitelist: [admin.super] exempts only super-admins (Validation.php:148)
Exploit: (Educational Purposes!)
<!-- Baseline: blocked --> <img src=x onerror=alert(1)> <!-- GHSA-269c payload: blocked (prior fix works) --> <img src=x =">" onerror=alert(1)> <!-- BYPASS A: passes (no XSS detected) --> <img src=x" onerror=alert(1)> <!-- BYPASS B: passes --> <img =x" onerror=alert(1)> <!-- BYPASS C: passes --> <a href=x" onmouseover=alert(1)>x</a> <!-- BYPASS D: passes --> <img src=x' onerror=alert(1)> <!-- BYPASS E: passes --> <div id=x" onmouseover=alert(1)>hover</div>
Protection: from this CVE
Recommended fix: update to Grav 2.0.15 or later Alternative: modify the on_events regex to treat a quote as a delimiter only in the after-= position, or normalize unquoted attribute values before the handler scan. Additional mitigation: encode " and ' in stored non-super content, or default markdown.escape_markup: true for non-super authors.
Impact:
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N (8.7). A non-super editor account can store a payload that executes script in the site origin against every visitor, including administrators viewing the content. The image `src` 404s and `onerror` fires automatically, requiring no user interaction beyond rendering the page. Other rules do not cover the payload because img/a/div are not in xss_dangerous_tags, and there is no javascript:/data: scheme or style/url/expression attribute.
🎯Let’s Practice Exploiting & Learn Patching For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

