{"record":{"id":"79e9838297d7b4de","repo":"getgrav/grav","slug":"twig-filter-arrow-is-not-allowed","errorCode":null,"errorMessage":"Twig |filter(\"{arrow}\") is not allowed.","messagePattern":"Twig \\|filter\\(\"(.+?)\"\\) is not allowed\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Common/Twig/Extension/GravExtension.php","lineNumber":2071,"sourceCode":"            'numeric' => is_numeric($var),\n            'object' => is_object($var),\n            'scalar' => is_scalar($var),\n            'string' => is_string($var),\n            default => false,\n        };\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 filterFunc(Environment $env, $array, $arrow)\n    {\n        if (!$arrow instanceof \\Closure && !is_string($arrow) || Utils::isDangerousFunction($arrow)) {\n            throw new RuntimeError('Twig |filter(\"' . $arrow . '\") is not allowed.');\n        }\n\n        if ($array === null) {\n            $array = [];\n        }\n\n        return twig_array_filter($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 mapFunc(Environment $env, $array, $arrow)\n    {","sourceCodeStart":2053,"sourceCodeEnd":2089,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Common/Twig/Extension/GravExtension.php#L2053-L2089","documentation":"Grav overrides Twig core's |filter and rejects the arrow before delegating to twig_array_filter when it is neither a \\Closure nor a string, or when Utils::isDangerousFunction() flags the string. That denylist covers command execution (exec, system, passthru, shell_exec, popen, proc_open, pcntl_exec), code execution (assert, preg_replace, create_function, include/require) and callback-style PHP functions. Without the guard, |filter('system') in an unsandboxed template would invoke system($v, $k) per element — remote code execution.","triggerScenarios":"A template containing {{ list|filter('system') }} or another denylisted function name (attack payload or leftover debug code); passing a null arrow ({{ list|filter(null-var) }} — null is neither Closure nor string, so it throws); array-style callables like ['MyClass', 'method'] which are neither Closure nor string; old Twig 1.x/2.x-era snippets using string callables.","commonSituations":"Migrating legacy themes to current Grav/Twig; attempted template injection visible in logs; static analysis flagging string callables in templates; passing uninitialized variables as the arrow.","solutions":["Rewrite the callback as a Twig arrow function: {{ list|filter(v => v.published) }}","For class-method callables, wrap them: {{ list|filter(v => MyClass::isVisible(v)) }}","If a denylisted name like system appears in a template, treat it as a security incident: audit the template source and how it was written, do not just patch around it","Ensure the arrow argument is never null — guard variables used as callbacks"],"exampleFix":"{# before: string callable (legacy or injected) #}\n{{ items|filter('system') }}\n\n{# after: arrow function Closure #}\n{{ items|filter(v => v.published) }}","handlingStrategy":"validation","validationCode":"// guard the arrow before render (mirrors filterFunc's check)\n$ok = $arrow instanceof \\Closure || (is_string($arrow) && !Utils::isDangerousFunction($arrow));\nif (!$ok) { $arrow = fn($v) => $v; // replace with a safe Closure 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_and_source($e->getSourceContext()); // audit, don't silently retry\n}","preventionTips":["Always write |filter callbacks as arrow functions: |filter(v => cond)","Never pass PHP function names as strings into template callbacks","Grep templates for |filter(' in CI and reject the pattern","Treat a dangerous-name hit as attempted template injection and audit its origin"],"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"}