Sep 17, 2026·7 min read·1 visit
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.
An OAuth resource spoofing vulnerability in the rmcp crate prior to 2.0.0 allows a malicious Model Context Protocol (MCP) server to spoof protected resource metadata. By presenting metadata pointing to a legitimate resource and authorization server, the attacker can trick the client into completing the authentication flow and subsequently sending the authorized token back to the malicious server.
CVE-2026-63128 is a high-severity uncontrolled resource consumption vulnerability in the Model Context Protocol (MCP) official Rust SDK (the rmcp crate) prior to version 2.0.0. An unauthenticated attacker can exploit this vulnerability by sending malformed or mismatching handshake requests to the stateful Streamable HTTP server, causing persistent memory allocation without cleanup. This results in an unbounded memory leak and lock contention that ultimately leads to complete denial of service.
A cross-site scripting (XSS) vulnerability was identified in @nuxtjs/mdc prior to version 0.22.1. Gaps in the HTML/SVG attribute verification and URL protocol parsing allow unauthenticated remote attackers to bypass the application's sanitization routines. By embedding malicious SVG links or data-encoded iframe elements within Markdown, attackers can execute arbitrary JavaScript in the victim's browser context.
CVE-2026-58657 is a critical stored CSS injection vulnerability in Grav CMS's media processing pipeline. By exploiting improper sanitization of image dimensions in the resize helper, low-privileged users with page editing permissions can inject arbitrary CSS styles. This can lead to visual defacement, UI redressing, and indirect data exfiltration.
An authorization-decision over-inclusion vulnerability exists in the OpenFGA authorization engine. The flaw manifests within the `ListUsers` API evaluation path when evaluating complex relationship intersections containing exclusions. Under certain configurations involving wildcards, the exclusion is bypassed, leading to incorrect permission lists.
An authorization bypass vulnerability exists in the djust framework (djust-org/djust) prior to version 1.0.7. The framework fails to enforce standard Django view-level authorization mechanisms, such as AccessMixins or dispatch decorators, when mounting reactive views over stateful transport layers (WebSockets and Server-Sent Events). Unauthenticated or low-privileged attackers can establish persistent connections to mount arbitrary protected views and execute state-changing event handlers.