Listen to this Post
The vulnerability resides in the `Security::detectXss()` function within system/src/Grav/Common/Security.php. This function is the core XSS detection mechanism for Grav, invoked both at save-time through `Validation::checkSafety()` and at render-time via detectXssInEditorContent(). The function relies on a `$patterns` array containing six regular expressions, all of which use the PCRE `u` (UTF-8) modifier. The `u` modifier instructs the PCRE engine to treat the subject string as UTF-8 encoded text. However, PHP’s PCRE engine has a documented behavior: when the subject string contains even a single byte sequence that is not valid UTF-8, `preg_match()` does not simply skip that byte or return zero matches. Instead, it returns `false` for the entire call, and `preg_last_error()` is set to PREG_BAD_UTF8_ERROR. The `detectXss()` function only checks the truthiness of the result using if (preg_match(...) || preg_match(...)). This means `false` and `0 matches` are indistinguishable to the calling code. A single stray invalid UTF-8 byte anywhere in a field’s value, not necessarily near the actual XSS payload, causes every one of the six checks to silently report “no XSS found”. Meanwhile, a real browser decoding the same bytes as UTF-8 (the encoding Grav serves pages as) does not fail open. It substitutes the invalid byte with one U+FFFD replacement character and renders the surrounding markup completely normally. The `` tag remains structurally intact, and the payload still fires. An attacker with a page-edit (“publisher”) account, without super-admin rights, can exploit this. They set page content to something like
Hello world \x80<img src=x onerror=alert(document.cookie)>, where `\x80` is a raw invalid UTF-8 byte. This payload can be delivered via any non-JSON submission path, such as the bundled Form plugin’s multipart/urlencoded field. `Validation::checkSafety()` runs `detectXss()` on the value, every `preg_match()` call returns false, so `detectXss()` returns `null` (“no violation”). The payload saves unmodified. Any visitor loading the page, including a super-admin browsing the public site, executes the attacker’s JavaScript in their session. This constitutes a stored XSS (CWE-79) root-caused by improper input validation (CWE-20) that fails open on malformed input.
DailyCVE Form:
Platform: Grav CMS
Version: Before 2.0.14
Vulnerability: Stored XSS
Severity: High
date: 2026-08-18
Prediction: 2026-08-25
What Undercode Say:
$patterns = [
'on_events' => '<(?:"[^"]"|\'[^\']\'|[^>"\'])?(?:[\s\x00-\x20\"\'\/]|"[^"]"|\'[^\']\')on\s[a-z]+\s=iu',
// ... five more, all with the /u modifier
];
foreach ($patterns as $name => $regex) {
if (!empty($enabled_rules[$name])) {
if (preg_match($regex, (string) $string) || preg_match($regex, $orig)) {
return $name;
}
// ...
}
}
return null;
$regex = '<(?:"[^"]"|\'[^\']\'|[^>"\'])?(?:[\s\x00-\x20\"\'\/]|"[^"]"|\'[^\']\')on\s[a-z]+\s=iu'; var_dump(preg_match($regex, "<img src=x onerror=alert(1)>")); // int(1) -- caught var_dump(preg_match($regex, "<img src=x \x80onerror=alert(1)>")); // bool(false), preg_last_error()==4
How Exploit: (Educational Purposes!)
- Attacker holds a page-edit (“publisher”) account without super-admin rights.
- Sets page content to `Hello world \x80
` (a raw invalid UTF-8 byte, deliverable via any non-JSON submission path — e.g. the bundled Form plugin’s multipart/urlencoded field, or any blueprint-validated field populated from a raw POST body — `$_POST` values are not UTF-8-validated by PHP).
– `Validation::checkSafety()` runs `detectXss()` on the value; every `preg_match()` call returnsfalse, so `detectXss()` returns `null` (“no violation”). The payload saves unmodified. - Any visitor (including a super-admin browsing the public site) loads the page; the browser renders the intact `
` element, executing the attacker’s JavaScript in the visitor’s session.
Protection: from this CVE
public static function detectXss($string, ?array $options = null): ?string
{
if (null === $string || !is_string($string) || empty($string)) {
return null;
}
// Fail closed: mb_check_encoding() validates the whole string up front
// and returns a normal boolean — it never "fails open" the way a
// /u-flagged preg_match() does on malformed input.
if (!mb_check_encoding($string, 'UTF-8')) {
return 'invalid_encoding';
}
// ... rest unchanged
}
Impact:
- Type: Stored XSS (CWE-79)
- Auth required: Page-edit (“publisher”) account, not super-admin
- Consequence: Arbitrary JavaScript execution in any visitor’s browser, including a super-admin who views the page — a cross-trust-boundary escalation from publisher to admin-equivalent action capability.
🎯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

