CVE-2026-75834: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') in getgrav grav
## Vulnerability Details **Component**: getgrav/grav core **File**: `system/src/Grav/Common/Security.php` **Function**: `detectXss()` (all six entries in the `$patterns` array use the PCRE `u` modifier), invoked from `Grav\Common\Data\Validation::checkSafety()` (the save-time XSS gate for any non-`security.xss_whitelist` account's blueprint field, including the page `content` field) and `detectXssInEditorContent()` (the render-time backstop for GHSA-2c4f-86xc-cr74) **CWE**: CWE-79 (Stored XSS), root-caused by CWE-20 (Improper Input Validation — fails open on malformed input) **Severity**: High **CVSS**: 8.0 — CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N ### Relationship to prior advisories This project's `detectXss()`/`checkSafety()` stack has been patched at least three times for the "page editor without super-admin rights stores an event handler that runs for site visitors" bug class: GHSA-9695-8fr9-hw5q / GHSA-c2q3-p4jr-c55f / GHSA-w8cg-7jcj-4vv2 (unquoted-attribute bypasses), GHSA-269c-h76q-8cxw (quoted-attribute-boundary bypass), GHSA-2c4f-86xc-cr74 (render-time Twig-assembled bypass). All three patched the **regex logic**. This is a different, lower-level defect: the PHP regex *engine* silently refuses to evaluate the pattern at all once the input contains one invalid UTF-8 byte, independent of what the regex logic says — no amount of regex-logic hardening fixes this. ### Root Cause Every pattern in `$patterns` uses the PCRE `u` (UTF-8) modifier. PHP's documented behavior: if the subject string contains even one byte sequence that is not valid UTF-8, `preg_match()` does not "skip" that byte or report "no match" — it returns `false` for the **entire call**, with `preg_last_error() === PREG_BAD_UTF8_ERROR`. `detectXss()` only checks truthiness (`if (preg_match(...) || preg_match(...))`), so `false` and "0 matches" are indistinguishable to the calling code. A single stray byte anywhere in a field's value — not even near the actual payload — makes every one of the six checks 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 `<img ... onerror=...>` tag is untouched structurally; the payload still fires. ### Vulnerable Code ```php $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; // reached even when the string contains <img onerror=...>, // as long as it also contains one invalid UTF-8 byte anywhere ``` Directly reproducible against the exact regex: ```php $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 ``` ### Attack Scenario 1. Attacker holds a page-edit ("publisher") account without super-admin rights. 2. Sets page content to `Hello world \x80<img src=x onerror=alert(document.cookie)>` (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). 3. `Validation::checkSafety()` runs `detectXss()` on the value; every `preg_match()` call returns `false`, so `detectXss()` returns `null` ("no violation"). The payload saves unmodified. 4. Any visitor (including a super-admin browsing the public site) loads the page; the browser renders the intact `<img onerror=...>` element, executing the attacker's JavaScript in the visitor's session. ### 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. ### Recommended Fix ```php 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 } ``` `Va
AI Analysis
Technical Summary
CVE-2026-75834 is a stored XSS vulnerability in Grav CMS prior to version 2.0.14. The issue is in the Security::detectXss() function where all XSS detection patterns use the PCRE /u (UTF-8) modifier. If a single invalid UTF-8 byte is present in the page content, preg_match() returns false for all patterns, bypassing the save-time XSS validation (Validation::checkSafety()). An authenticated attacker with page-edit permissions but without the security.xss_whitelist privilege can inject malicious JavaScript that executes in the browser of any visitor viewing the compromised page.
Potential Impact
An attacker with authenticated page-edit permissions can store malicious JavaScript code that executes in the browsers of visitors to the affected pages, potentially leading to session hijacking, defacement, or other client-side attacks. The vulnerability requires authentication but does not require elevated privileges beyond page editing. The CVSS 4.0 score is 5.1 (medium severity), reflecting the moderate impact and attack complexity.
Mitigation Recommendations
Upgrade Grav to version 2.0.14 or later where this vulnerability is fixed. Since the vulnerability is patched in 2.0.14, applying this official fix fully mitigates the issue. Until patched, restrict page-edit permissions to trusted users and consider additional input validation or content sanitization controls as temporary measures.
CVE-2026-75834: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') in getgrav grav
Description
## Vulnerability Details **Component**: getgrav/grav core **File**: `system/src/Grav/Common/Security.php` **Function**: `detectXss()` (all six entries in the `$patterns` array use the PCRE `u` modifier), invoked from `Grav\Common\Data\Validation::checkSafety()` (the save-time XSS gate for any non-`security.xss_whitelist` account's blueprint field, including the page `content` field) and `detectXssInEditorContent()` (the render-time backstop for GHSA-2c4f-86xc-cr74) **CWE**: CWE-79 (Stored XSS), root-caused by CWE-20 (Improper Input Validation — fails open on malformed input) **Severity**: High **CVSS**: 8.0 — CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N ### Relationship to prior advisories This project's `detectXss()`/`checkSafety()` stack has been patched at least three times for the "page editor without super-admin rights stores an event handler that runs for site visitors" bug class: GHSA-9695-8fr9-hw5q / GHSA-c2q3-p4jr-c55f / GHSA-w8cg-7jcj-4vv2 (unquoted-attribute bypasses), GHSA-269c-h76q-8cxw (quoted-attribute-boundary bypass), GHSA-2c4f-86xc-cr74 (render-time Twig-assembled bypass). All three patched the **regex logic**. This is a different, lower-level defect: the PHP regex *engine* silently refuses to evaluate the pattern at all once the input contains one invalid UTF-8 byte, independent of what the regex logic says — no amount of regex-logic hardening fixes this. ### Root Cause Every pattern in `$patterns` uses the PCRE `u` (UTF-8) modifier. PHP's documented behavior: if the subject string contains even one byte sequence that is not valid UTF-8, `preg_match()` does not "skip" that byte or report "no match" — it returns `false` for the **entire call**, with `preg_last_error() === PREG_BAD_UTF8_ERROR`. `detectXss()` only checks truthiness (`if (preg_match(...) || preg_match(...))`), so `false` and "0 matches" are indistinguishable to the calling code. A single stray byte anywhere in a field's value — not even near the actual payload — makes every one of the six checks 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 `<img ... onerror=...>` tag is untouched structurally; the payload still fires. ### Vulnerable Code ```php $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; // reached even when the string contains <img onerror=...>, // as long as it also contains one invalid UTF-8 byte anywhere ``` Directly reproducible against the exact regex: ```php $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 ``` ### Attack Scenario 1. Attacker holds a page-edit ("publisher") account without super-admin rights. 2. Sets page content to `Hello world \x80<img src=x onerror=alert(document.cookie)>` (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). 3. `Validation::checkSafety()` runs `detectXss()` on the value; every `preg_match()` call returns `false`, so `detectXss()` returns `null` ("no violation"). The payload saves unmodified. 4. Any visitor (including a super-admin browsing the public site) loads the page; the browser renders the intact `<img onerror=...>` element, executing the attacker's JavaScript in the visitor's session. ### 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. ### Recommended Fix ```php 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 } ``` `Va
CVSS v4.0
Score 5.1medium
Affected software
getgrav
grav
Run on your own infrastructure? Check whether these packages are installed with threat-finder — our free open-source scanner.
AI-Powered Analysis
Machine-generated threat intelligence
Technical Analysis
CVE-2026-75834 is a stored XSS vulnerability in Grav CMS prior to version 2.0.14. The issue is in the Security::detectXss() function where all XSS detection patterns use the PCRE /u (UTF-8) modifier. If a single invalid UTF-8 byte is present in the page content, preg_match() returns false for all patterns, bypassing the save-time XSS validation (Validation::checkSafety()). An authenticated attacker with page-edit permissions but without the security.xss_whitelist privilege can inject malicious JavaScript that executes in the browser of any visitor viewing the compromised page.
Potential Impact
An attacker with authenticated page-edit permissions can store malicious JavaScript code that executes in the browsers of visitors to the affected pages, potentially leading to session hijacking, defacement, or other client-side attacks. The vulnerability requires authentication but does not require elevated privileges beyond page editing. The CVSS 4.0 score is 5.1 (medium severity), reflecting the moderate impact and attack complexity.
Mitigation Recommendations
Upgrade Grav to version 2.0.14 or later where this vulnerability is fixed. Since the vulnerability is patched in 2.0.14, applying this official fix fully mitigates the issue. Until patched, restrict page-edit permissions to trusted users and consider additional input validation or content sanitization controls as temporary measures.
Technical Details
- Data Version
- 5.2
- Assigner Short Name
- VulnCheck
- Date Reserved
- 2026-08-18T10:57:39.580Z
- Cvss Version
- 4.0
- State
- PUBLISHED
Threat ID: 6a844366c6e8be03322294f6
Added to database: 08/18/2026, 11:35:02 UTC
Last enriched: 09/11/2026, 23:18:06 UTC
Last updated: 10/03/2026, 02:46:06 UTC
Views: 59
Community Reviews
0 reviewsCrowdsource mitigation strategies, share intel context, and vote on the most helpful responses. Sign in to add your voice and help keep defenders ahead.
Want to contribute mitigation steps or threat intel context? Sign in or create an account to join the community discussion.
Actions
Updates to AI analysis require Pro Console access. Upgrade inside Console → Billing.
Need more coverage?
Upgrade to Pro Console for AI refresh and higher limits.
For incident response and remediation, OffSeq services can help resolve threats faster.
Latest Threats
Check if your credentials are on the dark web
Instant breach scanning across billions of leaked records. Free tier available.