{"record":{"id":"8d8c9e6c61ca108f","repo":"twigphp/Twig","slug":"the-callable-passed-to-the-s-s-must-be-a-closure-in-sandbox","errorCode":null,"errorMessage":"The callable passed to the \"%s\" %s must be a Closure in sandbox mode.","messagePattern":"The callable passed to the \"(.+?)\" (.+?) must be a Closure in sandbox mode\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"src/Extension/CoreExtension.php","lineNumber":2189,"sourceCode":"            if (!$arrow($v, $k)) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * @internal\n     */\n    public static function checkArrow(bool $isSandboxed, $arrow, $thing, $type): void\n    {\n        if ($arrow instanceof \\Closure) {\n            return;\n        }\n\n        if ($isSandboxed) {\n            throw new RuntimeError(\\sprintf('The callable passed to the \"%s\" %s must be a Closure in sandbox mode.', $thing, $type));\n        }\n\n        trigger_deprecation('twig/twig', '3.15', 'Passing a callable that is not a PHP \\Closure as an argument to the \"%s\" %s is deprecated.', $thing, $type);\n    }\n\n    /**\n     * @internal to be removed in Twig 4\n     */\n    public static function captureOutput(iterable $body): string\n    {\n        $level = ob_get_level();\n        ob_start();\n\n        try {\n            foreach ($body as $data) {\n                echo $data;\n            }\n        } catch (\\Throwable $e) {","sourceCodeStart":2171,"sourceCodeEnd":2207,"githubUrl":"https://github.com/twigphp/Twig/blob/a414c3a491defb5a60f2fc88ef79ff37c90010cd/src/Extension/CoreExtension.php#L2171-L2207","documentation":"CoreExtension::checkArrow validates the arrow-function argument passed to `sort`, `filter`, `find`, `map`, `reduce`, `has some`, or `has every`. In sandbox mode every callable must be a PHP \\Closure, because arbitrary callables (like 'strlen' or [Object, 'method']) would let sandboxed templates invoke unvetted code. Twig throws this RuntimeError when a non-Closure callable is used under an enabled sandbox; outside the sandbox it merely triggers a deprecation (since Twig 3.15).","triggerScenarios":"Rendering a template with `|filter`, `|map`, `|sort`, `|reduce`, `find`, `has some/every` through a sandboxed Twig\\Environment (Extension\\SandboxExtension enabled) while the compiled call site passes a non-Closure callable — typically because the template was compiled by a non-sandboxed parser or a custom extension supplies a plain callable/string function name as the arrow argument.","commonSituations":"Applications embedding user-supplied templates with the sandbox enabled; cached compiled templates reused between sandboxed and non-sandboxed environments; custom extensions passing `callable` parameters (strings like 'trim') instead of closures; upgrading Twig to >=3.15 where this check was tightened.","solutions":["Ensure the arrow argument is a PHP \\Closure — in templates arrow functions `v => ...` compile to closures; in custom extension code wrap callables: `$arrow = \\Closure::fromCallable($callable);`.","Clear the Twig cache after enabling/changing the sandbox so templates recompile with sandbox-safe arrow expressions.","If the callable genuinely cannot be a Closure, expose it as a registered Twig function/filter instead of passing it as an argument.","Check the twig/twig version: on >=3.15 this is a hard sandbox error; restructure code that relied on string callables in sandboxed templates."],"exampleFix":"// before (PHP custom extension)\n$twig->addFilter(new TwigFilter('custom', function ($items) {\n    return twig_array_filter($items, 'my_predicate'); // string callable\n}));\n\n// after\n$twig->addFilter(new TwigFilter('custom', function ($items) {\n    return twig_array_filter($items, \\Closure::fromCallable('my_predicate'));\n}));","handlingStrategy":"validation","validationCode":"// PHP, before passing an arrow to filter/map/reduce under a sandbox\nfunction ensureClosureForSandbox(callable $arrow): \\Closure {\n    return $arrow instanceof \\Closure ? $arrow : \\Closure::fromCallable($arrow);\n}","typeGuard":"function isSandboxSafeArrow($arrow): bool { return $arrow instanceof \\Closure; }","tryCatchPattern":"try {\n    $html = $sandboxedTwig->render('user_template.html.twig', $context);\n} catch (\\Twig\\Error\\RuntimeError $e) {\n    if (str_contains($e->getMessage(), 'must be a Closure in sandbox mode')) {\n        // reject/fix the template or supply a closure-based implementation\n    } else { throw $e; }\n}","preventionTips":["Never pass string callables or [obj, 'method'] arrays as arrow arguments in sandboxed templates","Clear the compiled-template cache whenever sandbox settings change","In custom extensions, wrap callables with \\Closure::fromCallable() before handing them to twig_array_* helpers","Keep sandbox policy tests that render representative arrow-using templates"],"tags":["twig","sandbox","closure","security"],"backgroundTag":"type-mismatch","analyzedSha":"a414c3a491defb5a60f2fc88ef79ff37c90010cd","analyzedAt":"2026-09-13T15:10:46.849Z","contentChangedAt":"2026-09-13T15:10:46.849Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}