{"record":{"id":"223492696dea11c6","repo":"getgrav/grav","slug":"twig-find-arrow-is-not-allowed","errorCode":null,"errorMessage":"Twig |find(\"{arrow}\") is not allowed.","messagePattern":"Twig \\|find\\(\"(.+?)\"\\) is not allowed\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Common/Twig/Extension/GravExtension.php","lineNumber":2144,"sourceCode":"     * 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     *\n     * The resolved sandbox state is passed through to Twig, so a string callable is\n     * also refused whenever the render is sandboxed (GHSA-p6qj-p5m7-f62h). The\n     * denylist above is defense-in-depth, not the only guard.\n     *\n     * @param Environment $env\n     * @param bool $isSandboxed\n     * @param mixed $array\n     * @param callable|string $arrow\n     * @return mixed\n     * @throws RuntimeError\n     */\n    function findFunc(Environment $env, bool $isSandboxed, $array, $arrow)\n    {\n        if (!$arrow instanceof \\Closure && !is_string($arrow) || Utils::isDangerousFunction($arrow)) {\n            throw new RuntimeError('Twig |find(\"' . $arrow . '\") is not allowed.');\n        }\n\n        return CoreExtension::find($env, $isSandboxed, $array ?? [], $arrow);\n    }\n\n    /**\n     * Hardened `sort` filter. Same rationale as findFunc(): a string comparator\n     * such as `sort('system')` would otherwise be called as `system($a, $b)` when\n     * rendered outside the sandbox. Plain sorts (no comparator) are unaffected.\n     *\n     * The resolved sandbox state is passed through so a string comparator is refused\n     * in sandbox mode (GHSA-p6qj-p5m7-f62h); hardcoding it off left the denylist as\n     * the only guard, and the denylist does not list every two-argument callable.\n     *\n     * @param Environment $env\n     * @param bool $isSandboxed\n     * @param mixed $array\n     * @param callable|string|null $arrow","sourceCodeStart":2126,"sourceCodeEnd":2162,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Common/Twig/Extension/GravExtension.php#L2126-L2162","documentation":"Grav's hardened |find override checks the arrow up front — it must be a \\Closure or a string not flagged by Utils::isDangerousFunction() — and then delegates to CoreExtension::find with the resolved sandbox state. Twig core only rejects a dangerous string callable (e.g. find('system') invoked as system($v, $k)) inside the sandbox; Grav's denylist is deliberately defense-in-depth for renders outside the sandbox too, which is why a bad callable fails here regardless of sandbox state.","triggerScenarios":"{{ items|find('system') }} or any denylisted function name in any template; a null or array-callable arrow (fails the Closure-or-string check); template-injection payloads crafted for Twig's arrow-argument filters (find, sort, filter, map, reduce).","commonSituations":"Attack payloads in submitted content that reached a template render; legacy snippets using string callables; old tutorials for 'find first matching item' written with PHP function names.","solutions":["Rewrite the predicate as an arrow function: {{ items|find(v => v.slug == 'about') }}","Wrap class methods instead of array callables: {{ items|find(v => MyClass::matches(v)) }}","If a denylisted name appears in template source or page content, audit for injection — the guard firing is a security signal, not noise","Ensure the arrow argument is always provided and never null"],"exampleFix":"{# before: string callable #}\n{{ items|find('system') }}\n\n{# after: arrow function Closure #}\n{{ items|find(v => v.slug == 'about') }}","handlingStrategy":"validation","validationCode":"// guard the arrow before render (mirrors findFunc's check)\n$ok = $arrow instanceof \\Closure || (is_string($arrow) && !Utils::isDangerousFunction($arrow));\nif (!$ok) { $arrow = fn($v) => false; // 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); // treat dangerous-name hits as injection signals\n}","preventionTips":["Write |find predicates as arrow functions: |find(v => cond)","Never pass PHP function names as the find predicate","CI-grep for |find(' and reject the pattern","Keep Grav/Twig current so the hardened overrides stay in place"],"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"}