Skip to content
github.com: GitHub Security Advisory (GHSA-p6qj-p5m7-f62h) external site

github.com: GitHub Security Advisory (GHSA-p6qj-p5m7-f62h) external site

github.com September 4, 2026

The sortFunc wrapper added by the fix for GHSA-xx48-97m4-h7qm hardcodes Twig's $isSandboxed argument to false . That flag is the only thing that enforces Twig's Closure-only rule for callables, so inside the sandbox |sort accepts a plain function name where |map , |filter and |reduce correctly refuse. The remaining guard is a denylist, and it misses spl_autoload , which performs an include .

HEAD 02b042862a06c9d323d42ce5eb392c81de954851 , which is 2.0.17, and byte-identical to the shipped grav-admin-v2.0.17.zip .

system/src/Grav/Common/Twig/Extension/GravExtension.php:2156-2163 :

The second parameter of CoreExtension::sort(Environment $env, bool $isSandboxed, ...) is the flag Twig's own check reads ( vendor/twig/twig/src/Extension/CoreExtension.php:2109 ):

The siblings sixty lines above get this right. filterFunc (:2068), mapFunc (:2088) and reduceFunc (:2108) delegate through twig_array_filter / twig_array_map , which resolve the flag honestly ( vendor/twig/twig/src/Resources/core.php:491 calls twig_resolve_is_sandboxed($env) ).

findFunc (:2136) has the same hardcoded false , but find is not on allowed_filters , so only sort is reachable. sort is on allowed_filters ( system/config/security.yaml:229 ).

The attacker holds page-write ( admin.pages or api.pages.write ). They do not need admin.super and do not need admin.pages_twig . They save a page whose form frontmatter carries the payload in form.process.email.subject . The bundled Email plugin renders those params as @EmailVar: , which GravSourcePolicy sandboxes, and that is the exact vector named in GHSA-xx48-97m4-h7qm and fixed in GHSA-gh8j-q67c-j53f . No config opt-in is needed; security.twig_content.process_enabled stays false .

Submitting the form renders the param, sortFunc drops the sandbox flag, uasort invokes spl_autoload($path, $ext) , and PHP includes the file. The payload carrier is the attacker's own page .md file, which Grav stores verbatim, so no upload is required.

Arbitrary PHP execution as the web user, from page-editing rights only. This is the shape the GHSA-gh8j changelog described as "someone with only page-editing rights run commands on the server".

Proof of concept, executed

Against the released 2.0.17 tree, using the real Security::buildTwigSandboxPolicy() :

Blast radius: of 2146 internal functions, 838 pass isDangerousFunction and take two arguments. Besides spl_autoload , I executed error_log (log write), get_headers (SSRF), dba_open($f,'n') (truncated a file to empty), openssl_x509_export_to_file (created a file), and setcookie / setrawcookie , while header itself is denylisted.

Resolve the flag the way the sibling wrappers do, CoreExtension::sort($env, twig_resolve_is_sandboxed($env), ...) , in both sortFunc and findFunc . The denylist behind it is the same allowlist-versus-denylist shape as GHSA-f8wv-xp27-6gq7 and will keep producing misses on its own.

reduceFunc (:2108) calls twig_array_map , so |reduce silently behaves as map .