Sep 17, 2026·6 min read·16 visits
A critical security bypass in Grav CMS's XSS safety gate allows attackers to inject and store arbitrary HTML/JavaScript by passing invalid UTF-8 bytes or oversized elements, triggering silent engine failures.
CVE-2026-75834 is a stored Cross-Site Scripting (XSS) vulnerability in Grav CMS core, caused by a design flaw in its input validation wrapper Security::detectXss(). Regular expressions using the PCRE UTF-8 /u modifier fail-open when encountering invalid UTF-8 sequences or when the PCRE JIT stack limit is exhausted, allowing authenticated users with page-editing privileges to save malicious HTML and scripts.
The flat-file Content Management System Grav relies on a central input validation library to inspect, block, and clean potential security threats before they write to disk. This mechanism, implemented in Security::detectXss(), targets user input across all editable page structures. Under standard settings, the scanner flags cross-site scripting (XSS) indicators before any modifications parse into the physical markdown pages.
Because the underlying engine inspects markup on the server side prior to database-free file writes, any validation failure represents an instantaneous bypass of the platform's editor permissions. The vulnerability surface is accessible to any authenticated user with page-creation or page-modification roles. This includes authors and editors who typically lack unrestricted script execution capabilities.
By leveraging specific design gaps in PHP's PCRE execution model, malicious payloads can navigate through these safety checks without setting off the system's defensive alerts. The resulting injection results in high-integrity stored scripts that target admin and visitor sessions.
The root cause lies in how Grav's validation logic evaluates the output of PHP's standard PCRE regular expression execution engine. Specifically, preg_match returns 0 if no matching threat patterns are located, and 1 if a matching pattern is caught. However, if the evaluation itself cannot complete due to internal engine limitations or data structure mismatches, the function returns the boolean value false.
The logic used to evaluate this output performed a loose truthiness check. Because PHP treats both 0 (clean) and false (processing error) as falsy values, any operational error inside preg_match was treated as an assurance that the evaluated input string contained zero threats. This architecture represents a classic fail-open state, where error handling falls back to the least restrictive option.
Two independent paths trigger this fail-open state. First, the inclusion of the /u (UTF-8) modifier on all regex pattern definitions forces preg_match to immediately reject inputs containing structurally invalid UTF-8 byte sequences. Second, recursive backtracking limits during the evaluation of oversized markup blocks trigger PCRE JIT stack limit errors, which similarly crash the evaluation and return false.
The vulnerable implementation in system/src/Grav/Common/Security.php directly called preg_match inside a loop without checking for exact integer returns. Here is the conceptual vulnerable structure:
// Vulnerable evaluation logic in detectXss
foreach ($patterns as $name => $regex) {
if (preg_match($regex, $string)) {
return $name; // Threat flagged
}
}
return null; // Treated as safeThe remediated approach resolves this by executing the validation within a specialized private helper, patternMatches. This function explicitly checks for strict boolean outcomes and defaults to a fail-closed response (true) if any matching errors occur. Below is the secure implementation:
private static function patternMatches(string $regex, string $subject): bool
{
$result = preg_match($regex, $subject);
if ($result !== false) {
return (bool) $result;
}
if (preg_last_error() === PREG_JIT_STACKLIMIT_ERROR) {
$jit = ini_get('pcre.jit');
ini_set('pcre.jit', '0');
$result = preg_match($regex[0] . '(?:)' . substr($regex, 1), $subject);
ini_set('pcre.jit', (string) $jit);
if ($result !== false) {
return (bool) $result;
}
}
// Fail closed if any other error (such as backtracking limits) occurs
return true;
}Exploitation requires an authenticated editor account or an API exposure that feeds user input into the Grav page parser. Attackers can leverage either the character encoding bypass or the JIT stack limit exhaustion vulnerability to deliver standard script vectors. The injection payload remains inactive on the server but is stored in its raw format inside the flat-file markdown content.
In the invalid UTF-8 attack vector, the attacker inserts a single bad byte (such as hex \x80) immediately before a functional HTML element. When the security validation layer parses this input, the internal regular expressions fail immediately. The engine returns false, causing the system to write the page content to disk. When the page is subsequently requested by a client, the browser decodes the malformed byte sequence into a Unicode replacement character (such as U+FFFD) and executes the remaining HTML and JavaScript normally.
In the JIT stack exhaustion attack vector, the attacker provides a highly nested or padded tag structure. The recursive validation expressions run out of JIT stack memory, generating a PREG_JIT_STACKLIMIT_ERROR. The regex matching halts, returns false, and permits the storing of the script payload. Upon page render, the browser disregards the excessive padding and executes the accompanying javascript attribute.
The impact of a stored Cross-Site Scripting vulnerability in a Content Management System like Grav is high. Because administrative actions in the dashboard can be executed via client-side requests, a successfully injected script targeting an administrator can automate high-privilege activities. This includes creating new admin users, altering configurations, or utilizing system-level integrations to compromise the hosting environment.
Furthermore, because the payload executes in the browsers of ordinary site visitors, it can be used for session hijacking, drive-by malware delivery, or phishing campaigns. The scope change metric in CVSS v3.1 (S:C) highlights this transition, indicating that the vulnerability allows the compromise of resources beyond the security boundaries of the target platform.
The vulnerability holds a CVSS v3.1 base score of 5.4. While it requires low privileges (PR:L), the potential for administrative takeover elevated by administrative views changes the practical threat landscape to a high risk level in typical production environments.
The recommended remediation path is to upgrade the Grav core system to version 2.0.14 or later. This version contains the fully patched Security::detectXss logic, replacing raw evaluation checks with robust fail-closed mechanics and handling invalid UTF-8 transformations safely at the entry point.
If instant upgrades are not feasible, teams should apply the code changes in the official patch commit 5192316f5bf0f033636f43561829d7fc13a4ab64. Administrators can also implement a Web Application Firewall (WAF) or local validation rules to strip malformed character byte strings (such as isolated non-ASCII characters) and restrict input sizes on page update endpoints.
To identify historical indicators of compromise, security teams should execute binary scans across the user/pages/ folder. Files failing UTF-8 compilation or containing excessively long HTML structures paired with Javascript event keywords should be flagged and manually validated.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
grav getgrav | < 2.0.14 | 2.0.14 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network (Authenticated Page-Edit Permissions Required) |
| CVSS v3.1 Score | 5.4 (Medium) |
| EPSS Score | 0.00182 (0.18% Probability) |
| Impact | Stored Cross-Site Scripting (XSS) / Account Takeover |
| Exploit Status | Proof of Concept (PoC) documented |
| CISA KEV Status | Not listed |
The software does not neutralize or incorrectly neutralizes user-controlled input before placing it in output that is served to other users, allowing malicious scripts to execute.
CVE-2026-118777 is a prototype pollution vulnerability (CWE-1321) in the Forge JavaScript cryptography and networking library (distributed on npm as node-forge) prior to version 1.4.1. The flaw resides in the forge.http client subsystem, where incoming HTTP cookie names and paths parsed from Set-Cookie response headers are stored directly into standard JavaScript objects without key validation or property isolation. By returning crafted Set-Cookie headers containing keys such as __proto__ or constructor, a malicious HTTP server can inject arbitrary properties into Object.prototype, attach attributes to the global Object constructor, or delete built-in prototype methods to cause an application-wide denial of service.
CVE-2026-118775 (GHSA-4642-5mcj-2rqw) is an integer overflow and validation flaw in the ASN.1 serialization module of Forge (node-forge) prior to version 1.4.1. The asn1.oidToDer() function fails to validate the range of the first two arcs of an Object Identifier (OID) and truncates the packed initial subidentifier to a single 8-bit integer. Applications that perform policy checks on untrusted OID strings before encoding them into Distinguished Encoding Rules (DER) format can be tricked into emitting DER bytes that correspond to a completely different, restricted OID.
CVE-2026-119194 is an algorithmic complexity vulnerability (CWE-407) in the pyasn1 library for Python, affecting versions 0.3.3 through 0.6.4. The flaw resides in the schemaless decoding routine ConstructedPayloadDecoderBase._decodeComponentsSchemaless used across BER, CER, and DER codecs. By sending an ASN.1 container populated with child components whose tag identifiers are separated by multiples of CPython's integer hash modulus (sys.hash_info.modulus), an unauthenticated attacker can force worst-case hash collisions in an unbounded Python set, degrading decoding performance from O(N) to O(N^2) and causing a Denial of Service (DoS).
CVE-2026-119195 is an algorithmic complexity vulnerability (CWE-407) in the pyasn1 library for Python, affecting all versions prior to 0.6.5. When decoding constructed strings or indefinite-length ANY types across BER, CER, and certain DER code paths, the payload decoders repeatedly concatenate incoming fragments onto an immutable bytes object using the += operator. Because Python bytes objects cannot be resized in-place, each fragment iteration allocates a new buffer and copies all previously accumulated bytes, resulting in quadratic O(N^2) time and memory copy complexity. An unauthenticated remote attacker can supply a highly fragmented ASN.1 stream to exhaust CPU cycles and stall application worker threads.
An algorithmic complexity vulnerability (CWE-407) in pyasn1 versions prior to 0.6.5 allows unauthenticated attackers to cause a Denial of Service (DoS) via crafted ASN.1 payloads. The flaw resides in the binary-encoded ASN.1 REAL mantissa deserialization routine within RealPayloadDecoder.valueDecoder. By performing iterative byte-string slicing and repeated bitwise shifts on a growing arbitrary-precision integer, the decoder exhibits quadratic time and memory allocation complexity relative to the mantissa length. Supplying a BER-, CER-, or DER-encoded payload with a large REAL mantissa causes excessive CPU consumption and blocks application worker processes.
A vulnerability classified under CWE-407 (Inefficient Algorithmic Complexity) exists in the pyasn1 library prior to version 0.6.5. When parsing constructed ASN.1 BIT STRING payloads under BER, CER, or schema-driven DER encoding rules, the BitStringPayloadDecoder repeatedly shifts and copies the entire accumulated bit value for each incoming fragment. Processing inputs with a large number of small constructed fragments induces quadratic CPU consumption, allowing unauthenticated remote attackers to cause a denial of service via resource exhaustion during the initial decoding phase.