Executive brief
Grav is a flat-file content management system used to build websites without a database. The system's XSS validator checks page content for dangerous scripts before storing it, but when Twig template processing is enabled, attackers can bypass this check by using Twig's string concatenation operator to hide malicious code until it is rendered. An attacker with page-write permissions can inject stored XSS that executes for all visitors, potentially stealing session cookies or defacing the site.
Technical details
The vulnerability is a two-stage stored XSS (CWE-79) in Grav's Security::detectXss() validator. The validator runs on raw page content before Twig template processing and uses regex patterns to block dangerous tags, event handlers, and protocols. However, when Twig content processing is enabled (twig_content.process_enabled: true), an attacker with api.pages.write permission can use Twig's string concatenation operator (~) to dynamically construct these dangerous patterns at render time—for example, {% set x = "on" ~ "error" %} followed by <img src=1 {{ x }}=alert(1)>. The validator sees only the harmless Twig expression and allows the content; the dangerous regexes (on_events, invalid_protocols, dangerous_tags, html_inline_styles) never match. After Twig renders the template, the output contains active payloads such as <img src=1 onerror=alert(1)> which are rendered via {{ page.content|raw }} with no escaping, executing arbitrary JavaScript in visitors' browsers. The attack also bypasses blocklists for dangerous tags (<script>, <iframe>, <svg>) and invalid protocols (javascript:, data:). Grav fixed this in version 2.0.1 by re-validating Twig-rendered output before serving it.
Affected products
- Getgrav Grav 2.0.0
Timeline
- 2026-06-24: disclosed: GHSA-2c4f-86xc-cr74 published by Getgrav security advisories
- 2026-06-24: patched: Patched in Grav 2.0.1