{"record":{"id":"d41d07bc9680a941","repo":"getgrav/grav","slug":"twig-reduce-arrow-is-not-allowed","errorCode":null,"errorMessage":"Twig |reduce(\"{arrow}\") is not allowed.","messagePattern":"Twig \\|reduce\\(\"(.+?)\"\\) is not allowed\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Common/Twig/Extension/GravExtension.php","lineNumber":2111,"sourceCode":"\n        if ($array === null) {\n            $array = [];\n        }\n\n        return twig_array_map($env, $array, $arrow);\n    }\n\n    /**\n     * @param Environment $env\n     * @param array $array\n     * @param callable|string $arrow\n     * @return array|CallbackFilterIterator\n     * @throws RuntimeError\n     */\n    function reduceFunc(Environment $env, $array, $arrow)\n    {\n        if (!$arrow instanceof \\Closure && !is_string($arrow) || Utils::isDangerousFunction($arrow)) {\n            throw new RuntimeError('Twig |reduce(\"' . $arrow . '\") is not allowed.');\n        }\n\n        if ($array === null) {\n            $array = [];\n        }\n\n        return twig_array_map($env, $array, $arrow);\n    }\n\n    /**\n     * Hardened `find` filter. Twig core only rejects a dangerous string callable\n     * (e.g. `find('system')`, invoked as `system($v, $k)`) when the template is\n     * sandboxed. Editor-authorable strings rendered OUTSIDE the sandbox — such as\n     * the Email plugin's form action params — reached that unguarded call and gave\n     * a page editor RCE (GHSA-xx48-97m4-h7qm). Apply the same dangerous-arrow guard\n     * used by filter/map/reduce, regardless of sandbox state; a real arrow closure\n     * still passes.\n     *","sourceCodeStart":2093,"sourceCodeEnd":2129,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Common/Twig/Extension/GravExtension.php#L2093-L2129","documentation":"Grav's override of Twig's |reduce applies the same guard as |filter and |map: the arrow must be a \\Closure or a string that is not flagged by Utils::isDangerousFunction(), otherwise a RuntimeError is thrown before delegation. A string like 'system' passed to reduce would be invoked as system($accumulator, $value) outside the sandbox, hence the refusal. Note that in this codebase reduceFunc currently delegates to twig_array_map rather than a reduce implementation — if |reduce silently behaves like |map after passing the guard, check your Grav version against upstream.","triggerScenarios":"{{ items|reduce('system') }} or any denylisted function name; passing a null arrow or an array callable (['MyClass', 'combine']) which is neither Closure nor string; legacy templates written against Twig versions where string callables were accepted unchecked.","commonSituations":"Migrating pre-hardening themes; aggregation snippets (summing, concatenating) written with string callables; injected templates tripping the RCE guard.","solutions":["Rewrite the reduction as an arrow function: {{ items|reduce((carry, v) => carry + v.price, 0) }}","Wrap class methods instead of array callables: {{ items|reduce((carry, v) => MyClass::combine(carry, v), '') }}","Treat denylisted names in templates as a security signal and audit the template's origin","Never pass an uninitialized (null) arrow to |reduce"],"exampleFix":"{# before: string callable #}\n{{ items|reduce('system') }}\n\n{# after: arrow function Closure #}\n{{ items|reduce((carry, v) => carry + v.price, 0) }}","handlingStrategy":"validation","validationCode":"// guard the arrow before render (mirrors reduceFunc's check)\n$ok = $arrow instanceof \\Closure || (is_string($arrow) && !Utils::isDangerousFunction($arrow));\nif (!$ok) { $arrow = fn($carry, $v) => $carry; // safe default or fail fast\n}","typeGuard":"function isSafeTwigArrow(mixed $arrow): bool\n{\n    return $arrow instanceof \\Closure || (is_string($arrow) && !\\Grav\\Common\\Utils::isDangerousFunction($arrow));\n}","tryCatchPattern":"use Twig\\Error\\RuntimeError;\ntry { echo $twig->render($template, $data); }\ncatch (RuntimeError $e) { log_template_error($e); // audit template provenance\n}","preventionTips":["Write |reduce callbacks as arrow functions: |reduce((carry, v) => expr, initial)","Migrate legacy string-callable reductions when upgrading Twig/Grav","CI-grep for |reduce(' and reject","Audit any denylisted function name found in a template"],"tags":["twig","security","filter","callable","rce-guard"],"backgroundTag":"twig-unsafe-callable","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}