CVE-2026-75834
Grav before 2.0.14 Stored XSS via Invalid UTF-8 Byte
Grav before 2.0.14 contains a stored cross-site scripting vulnerability in the Security::detectXss() function (system/src/Grav/Common/Security.php). All XSS detection patterns use the PCRE /u (UTF-8) modifier, so a single invalid UTF-8 byte anywhere in page content causes preg_match() to return false for every pattern, silently bypassing the save-time XSS safety gate (Validation::checkSafety()). An authenticated attacker with page-edit permissions (without the security.xss_whitelist privilege) can store malicious JavaScript that executes in the browser of a visitor who views the affected page.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- Passive
What an attacker can affect
- Confidentiality
- None
- Integrity
- None
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
5.1
Grav before 2.0.14 Stored XSS via Invalid UTF-8 Byte
Grav before 2.0.14 contains a stored cross-site scripting vulnerability in the Security::detectXss() function (system/src/Grav/Common/Security.php). All XSS detection patterns use the PCRE /u (UTF-8) modifier, so a single invalid UTF-8 byte anywhere in page content causes preg_match() to return false for every pattern, silently bypassing the save-time XSS safety gate (Validation::checkSafety()). An authenticated attacker with page-edit permissions (without the security.xss_whitelist privilege) can store malicious JavaScript that executes in the browser of a visitor who views the affected page.
- Record
CVE-2026-75834- CVSS 4.0
- 5.1 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N - CVSS 3.1
- 5.4 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
EUVDEUVD-2026-60598
5.1
Grav before 2.0.14 contains a stored cross-site scripting vulnerability in the Security::detectXss() function (system/src/Grav/Common/Security.php). All XSS detection patterns use the PCRE /u (UTF-8) modifier, so a single invalid UTF-8 byte anywhere in page content causes preg_match() to return false for every pattern, silently bypassing the save-time XSS safety gate (Validation::checkSafety()). An authenticated attacker with page-edit permissions (without the security.xss_whitelist privilege) can store malicious JavaScript that executes in the browser of a visitor who views the affected page.
- Record
EUVD-2026-60598- CVSS 4.0
- 5.1 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-993v-76jg-67xr
5.1
Duplicate Advisory: Grav: Single invalid UTF-8 byte disables every rule in Security::detectXss(), bypassing the page-content XSS safety gate
Duplicate Advisory
This advisory has been withdrawn because it is a duplicate of GHSA-q2j8-x8hf-63ch. This link is maintained to preserve external references.
Original Description
Grav before 2.0.14 contains a stored cross-site scripting vulnerability in the Security::detectXss() function (system/src/Grav/Common/Security.php). All XSS detection patterns use the PCRE /u (UTF-8) modifier, so a single invalid UTF-8 byte anywhere in page content causes preg_match() to return false for every pattern, silently bypassing the save-time XSS safety gate (Validation::checkSafety()). An authenticated attacker with page-edit permissions (without the security.xss_whitelist privilege) can store malicious JavaScript that executes in the browser of a visitor who views the affected page.
- Record
GHSA-993v-76jg-67xr- CVSS 4.0
- 5.1 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - CVSS 3.1
- 5.4 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
GitHub AdvisoriesGHSA-q2j8-x8hf-63ch
5.1
Grav: Single invalid UTF-8 byte disables every rule in Security::detectXss(), bypassing the page-content XSS safety gate
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
$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:
$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
- Attacker holds a page-edit ("publisher") account without super-admin rights.
- 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 —$_POSTvalues are not UTF-8-validated by PHP). Validation::checkSafety()runsdetectXss()on the value; everypreg_match()call returnsfalse, sodetectXss()returnsnull("no violation"). The payload saves unmodified.- Any visitor (including a super-admin…
- Record
GHSA-q2j8-x8hf-63ch- CVSS 4.0
- 5.1 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N - CVSS 3.1
- 5.4 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
NVD
5.1
Grav before 2.0.14 contains a stored cross-site scripting vulnerability in the Security::detectXss() function (system/src/Grav/Common/Security.php). All XSS detection patterns use the PCRE /u (UTF-8) modifier, so a single invalid UTF-8 byte anywhere in page content causes preg_match() to return false for every pattern, silently bypassing the save-time XSS safety gate (Validation::checkSafety()). An authenticated attacker with page-edit permissions (without the security.xss_whitelist privilege) can store malicious JavaScript that executes in the browser of a visitor who views the affected page.
- Record
CVE-2026-75834- CVSS 4.0
- 5.1 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - CVSS 3.1
- 5.4 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
OSV
5.1
Grav before 2.0.14 Stored XSS via Invalid UTF-8 Byte
Grav before 2.0.14 contains a stored cross-site scripting vulnerability in the Security::detectXss() function (system/src/Grav/Common/Security.php). All XSS detection patterns use the PCRE /u (UTF-8) modifier, so a single invalid UTF-8 byte anywhere in page content causes preg_match() to return false for every pattern, silently bypassing the save-time XSS safety gate (Validation::checkSafety()). An authenticated attacker with page-edit permissions (without the security.xss_whitelist privilege) can store malicious JavaScript that executes in the browser of a visitor who views the affected page.
- Record
CVE-2026-75834- CVSS 4.0
- 5.1 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N - Weakness
- CWE-79
- Updated
- 2026-11-02
OSVGHSA-993v-76jg-67xr
5.1
Duplicate Advisory: Grav: Single invalid UTF-8 byte disables every rule in Security::detectXss(), bypassing the page-content XSS safety gate
Duplicate Advisory
This advisory has been withdrawn because it is a duplicate of GHSA-q2j8-x8hf-63ch. This link is maintained to preserve external references.
Original Description
Grav before 2.0.14 contains a stored cross-site scripting vulnerability in the Security::detectXss() function (system/src/Grav/Common/Security.php). All XSS detection patterns use the PCRE /u (UTF-8) modifier, so a single invalid UTF-8 byte anywhere in page content causes preg_match() to return false for every pattern, silently bypassing the save-time XSS safety gate (Validation::checkSafety()). An authenticated attacker with page-edit permissions (without the security.xss_whitelist privilege) can store malicious JavaScript that executes in the browser of a visitor who views the affected page.
- Record
GHSA-993v-76jg-67xr- CVSS 4.0
- 5.1 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - CVSS 3.1
- 5.4 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
OSVGHSA-q2j8-x8hf-63ch
5.1
Grav: Single invalid UTF-8 byte disables every rule in Security::detectXss(), bypassing the page-content XSS safety gate
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
$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:
$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
- Attacker holds a page-edit ("publisher") account without super-admin rights.
- 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 —$_POSTvalues are not UTF-8-validated by PHP). Validation::checkSafety()runsdetectXss()on the value; everypreg_match()call returnsfalse, sodetectXss()returnsnull("no violation"). The payload saves unmodified.- Any visitor (including a super-admin…
- Record
GHSA-q2j8-x8hf-63ch- CVSS 4.0
- 5.1 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N - CVSS 3.1
- 5.4 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
getgrav/gravPackagist |
before 2.0.14 | 2.0.14 |
getgrav/gravPackagist |
before 2.0.14, plus listed versions 0.8.00.9.00.9.10.9.100.9.11 Show 45 moreShow less0.9.120.9.130.9.140.9.150.9.160.9.170.9.180.9.190.9.20.9.200.9.210.9.220.9.230.9.240.9.250.9.260.9.270.9.280.9.290.9.30.9.300.9.310.9.320.9.330.9.340.9.350.9.360.9.370.9.380.9.390.9.40.9.400.9.410.9.420.9.430.9.440.9.450.9.50.9.60.9.70.9.80.9.91.0.01.0.0-rc.11.0.0-rc.2 |
2.0.14 |
History
| Recorded | Changed | Sources |
|---|---|---|
| affected | ||
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-75834 is a medium vulnerability (CVSS 5.1) in Getgrav Grav, published August 18, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.26%, the 17th percentile. Fixed in getgrav/grav 2.0.14.
References
github.com/getgrav/grav/security/advisories/GHSA-q2j8-x8hf-63ch
vulncheck.com/advisories/grav-before-stored-xss-via-invalid-utf-8-byte
nvd.nist.gov/vuln/detail/CVE-2026-75834
github.com/advisories/GHSA-993v-76jg-67xr
github.com/advisories/GHSA-q2j8-x8hf-63ch
github.com/CVEProject/cvelistV5/tree/main/cves/2026/75xxx/CVE-2026-75834.json
github.com/getgrav/grav
Left out here: 298 versions and the rest of 2 source descriptions. The JSON record has them.