Sep 17, 2026·7 min read·44 visits
A stored XSS vulnerability in Grav CMS before v2.0.1 allows attackers with page-write permissions to bypass blueprint validation filters using Twig string concatenation. The vulnerability is fixed in version 2.0.1 by introducing a post-render scanning step.
Grav CMS before v2.0.1 contains a security bypass vulnerability in its blueprint validation logic. The XSS detection routine, Security::detectXss(), was executed on raw page contents prior to Twig engine processing. When Twig processing is enabled for editor-authored page content, an attacker can dynamically reconstruct harmful HTML elements, attributes, or protocols using string concatenation (e.g. `{{ 'on' ~ 'error' }}`). When compiled, the benign source converts into active XSS payloads, which are rendered to the client browser via raw filters. This vulnerability was resolved in version 2.0.1 by adding a post-render validation backstop.
Grav CMS is an open-source, flat-file content management system that relies on a modular architecture and Twig templating for page rendering. To prevent Cross-Site Scripting (XSS) attacks, the system utilizes a custom validation class, Security::detectXss(), to scan incoming content before saving pages. However, a structural limitation exists because this verification takes place during the blueprint validation phase, which exclusively inspects raw input before template compile cycles.
When Twig processing is enabled for page content through the twig_content.process_enabled directive, the input can contain both markdown markup and Twig syntax. An attacker with page-write API permissions can exploit this sequence by structuring Twig code that dynamically generates HTML tags, attributes, or protocols during template execution. Because the input validation occurs before Twig compiles the template, the raw code appears entirely benign to the security filters.
Upon template execution, the Twig engine resolves the dynamic expressions into executable HTML payloads. Because Grav prints page content through unescaped filters, specifically {{ page.content|raw }}, the browser executes the compiled payload, leading to stored Cross-Site Scripting. This flaw is tracked as CVE-2026-61453 and GHSA-2c4f-86xc-cr74.
The root cause of this vulnerability lies in the sequence of operations within the content rendering pipeline of Grav CMS. The application performs input sanitization at the wrong stage of the lifecycle. By analyzing raw input files prior to template parsing, the system exposes an evasion surface where compiled assets are never re-evaluated.
During the blueprint validation phase, Grav processes input using standard regex checks inside Security::detectXss(). This validator identifies common XSS signatures such as <script> tags, dangerous JavaScript protocols like javascript:, and HTML event attributes including onerror or onload. This approach relies on the assumption that the input matches the final output structure.
This assumption fails when the page content contains template instructions. Twig allows string operations, variable substitution, and character concatenation. An attacker can write template statements like {{ "on" ~ "error" }} or <s{{ "c"~"r"~"i"~"p"~"t" }}>. To the raw string scanner, these represent harmless Twig syntax, bypassing validation. When the compiler evaluates the templates, the server dynamically pieces together the forbidden elements and generates the malicious HTML payload, which is then rendered directly into the browser.
In vulnerable versions of Grav CMS, the Page::content() method retrieves, caches, and processes markdown and Twig templates. When Twig processing is requested, the application executes $this->processTwig() without tracking the origin or trust level of the template source. Crucially, the system does not recheck the compiled HTML output of the Twig engine for security policy violations.
The patch resolves this gap by introducing a post-render validation check. When the system executes Twig templates sourced from user input, the application now forwards an evaluation flag $scan_twig_xss. After compiling the template, the updated code executes Security::detectXss() directly on the generated HTML string.
The following code diff shows the implementation of this validation check in system/src/Grav/Common/Page/Page.php:
// Editor-authored content Twig (gated by process_enabled) gets its
// rendered output re-scanned for XSS; trusted modular/theme Twig
// does not. (GHSA-2c4f-86xc-cr74)
$scan_twig_xss = $content_twig_requested && $content_twig_allowed;
$process_twig = $scan_twig_xss || $this->modularTwig();
// ... inside content generation pipeline ...
if ($process_twig) {
$this->processTwig($scan_twig_xss);
}
// Inside modified processTwig function:
private function processTwig(bool $scanXss = false)
{
/** @var Twig $twig */
$twig = Grav::instance()['twig'];
$this->content = $twig->processPage($this, $this->content);
if ($scanXss && is_string($this->content) && $this->content !== ''
&& (bool) Grav::instance()['config']->get('security.twig_content.xss_scan_output', true)) {
$found = Security::detectXss($this->content);
if ($found !== null) {
Security::logTwigContentXssBlocked((string) ($this->route() ?? $this->filePath() ?? 'unknown'), $found);
$this->content = '';
}
}
}This post-render check serves as an effective control because it intercepts the payload after all Twig evaluations are completed but before the HTML is sent to the client. If any prohibited element is identified within $this->content, the rendering process logs the event and sets the content to an empty string.
To exploit this vulnerability, an attacker must have privileges to write or edit pages within the Grav CMS installation, or exploit an API endpoint that permits page modification. The host must also have the twig_content.process_enabled directive set to true to force Twig compilation on page elements.
The attacker structures a payload designed to construct executable Javascript from benign components. The easiest method leverages Twig's concatenation operator (~). By breaking down restricted words into separate string segments, the attacker evades static regex evaluation during the initial save.
{# Exploiting attribute detection using string concatenation #}
<img src="invalid_image.jpg" {{ "on" ~ "error" }}="alert(document.domain)">
{# Exploiting tag detection by assembling the tag name #}
<s{{ "c" ~ "r" ~ "i" ~ "p" ~ "t" }}>alert(document.domain)</s{{ "c" ~ "r" ~ "i" ~ "p" ~ "t" }}>
{# Exploiting protocol filters using inline string evaluation #}
<a href="{{ "java" ~ "script" }}:alert(1)">Click to continue</a>When an administrator or visitor navigates to the modified page, the server executes the Twig processor. The rendered response contains the assembled HTML fragments, executing the script payloads in the context of the user's browser session.
Here is a diagram representing the vulnerability and validation bypass flow:
The security impact of CVE-2026-61453 is substantial, matching the standard risks associated with stored Cross-Site Scripting. Since the payload is stored within the page content on the file system, every user visiting the affected page will execute the injected script.
If an administrative user visits the compromised page, the attacker can execute administrative actions. This includes creating new admin accounts, modifying site configurations, or installing malicious plugins. In flat-file CMS architectures like Grav, possessing administrator access often permits arbitrary file uploads, which can be leveraged to write PHP backdoors and achieve remote code execution (RCE) on the underlying server.
For general visitors, the vulnerability can be used to perform browser session hijacking, steal session tokens, or execute drive-by attacks. The vulnerability carries a CVSS v4.0 score of 5.1 and a CVSS v3.x score of 6.1. Although the prerequisite requires page-authoring or write permissions, any vulnerability that allows lower-privileged users or API integrations to execute script in the administrative context presents a significant path to full system compromise.
The primary remediation path is upgrading the Grav CMS installation to version 2.0.1 or higher, which implements the post-render validation check. If an immediate upgrade is not possible, administrators should evaluate security policies and apply mitigation settings.
First, verify that the Twig content processing configuration is disabled if it is not strictly required. The configuration can be modified in /user/config/security.yaml by applying the following settings:
twig_content:
process_enabled: falseIf Twig processing must remain enabled, manually apply the post-render scan configuration to the site's configuration. Ensure that the xss_scan_output parameter is explicitly configured:
twig_content:
process_enabled: true
xss_scan_output: trueFurthermore, configure web application firewalls (WAF) to detect Twig-specific delimiters and concatenation characters (~) within page modification API calls. This step adds an extra layer of defense against dynamic bypass techniques.
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.1 | 2.0.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network |
| CVSS v3.1 Score | 6.1 (Medium) |
| EPSS Score | 0.00263 (Percentile: 18.26%) |
| Impact | Stored Cross-Site Scripting (XSS) / Account Takeover |
| Exploit Status | PoC (Proof of Concept) available |
| CISA KEV Status | Not Listed |
The application does not neutralize or incorrectly neutralizes user-controlled input before rendering it as HTML, enabling attackers to execute malicious scripts in client browsers.
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.