In-app reader
Grav 2.0 renders editor-authored Twig in page content by default and relies on the Twig content sandbox to contain it. The shipped sandbox policy allowlists addcss and addjs on Grav\Common\Assets (system/src/Grav/Common/Twig/Sandbox/SandboxDefaults.php:307). Because the sandbox arbitrates the call and not its downstream effect, a user holding only page-edit rights can register an arbitrary asset from page content; the theme then emits it into the document head as a / tag. The asset URL is concatenated into that tag without escaping, so it can also break out of its own attribute.
The save-time XSS scan cannot see this: Security::detectXssInEditorContent() renders the content body in isolation and inspects the returned string, while assets.addJs() acts by mutating the shared Assets service and returns only an object key. The payload contains no markup for the scanner to flag.
This is not a Security::detectXss() bypass. It is content reaching an unescaped output sink through an allowlisted method.
All Grav 2.0 releases whose sandbox policy allowlists addcss/addjs on Grav\Common\Assets. The entry predates 2.0.19 — it was carried forward unchanged when the sandbox allowlists moved from system/config/security.yaml into SandboxDefaults in 2.0.19.
Grav 1.7 is not affected: it has no Twig content sandbox and required an explicit per-page process: twig.
Reachable by a plain page editor, with no Twig permission and no configuration change. On a stock install security.twig_content.process_enabled is true and system/config/system.yaml ships process: { markdown: true } with no twig key, so Security::applyTwigContentDefault() defaults every page's process.twig to the gate's value. Content Twig therefore runs on every page that does not explicitly set the flag. security.twig_content.editor_enabled: false and the admin.pages_twig permission gate only the per-page override checkbox in the editor — they do not gate whether Twig runs.
The sink. Assets/Js.php:46 (and identically Css.php:50, Link.php:41, JsModule.php:47) builds the tag by concatenation with no escaping:
return '' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . ...
Discussion
Sign in to join the discussion.
Keep reading
Optional: create a free account to save items, track programs, and sync across web + app. Reading stays free.