{"record":{"id":"a7e25f6629941b7d","repo":"getgrav/grav","slug":"filter-s-is-not-allowed-on-deeply-nested-data-i","errorCode":null,"errorMessage":"Filter \"%s\" is not allowed on deeply nested data inside sandboxed content.","messagePattern":"Filter \"(.+?)\" is not allowed on deeply nested data inside sandboxed content\\.","errorType":"exception","errorClass":"SecurityNotAllowedFilterError","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Common/Twig/Extension/GravExtension.php","lineNumber":373,"sourceCode":"\n    /**\n     * Recursive worker for assertSandboxDumpSafe(). Throws\n     * SecurityNotAllowedFilterError on the first object that is not permitted, so\n     * the sandboxed render soft-fails and logs the violation like any other\n     * sandbox block.\n     *\n     * @param GravSecurityPolicy|null $policy\n     * @param mixed $var\n     * @param string $filter\n     * @param bool $reflective\n     * @param int $depth\n     * @return void\n     */\n    private function scanSandboxDump(?GravSecurityPolicy $policy, mixed $var, string $filter, bool $reflective, int $depth = 0): void\n    {\n        if ($depth > 16) {\n            // Pathological nesting / cycles: refuse rather than recurse forever.\n            throw new SecurityNotAllowedFilterError(\n                sprintf('Filter \"%s\" is not allowed on deeply nested data inside sandboxed content.', $filter),\n                $filter\n            );\n        }\n\n        if (is_array($var)) {\n            foreach ($var as $item) {\n                $this->scanSandboxDump($policy, $item, $filter, $reflective, $depth + 1);\n            }\n            return;\n        }\n\n        if (!is_object($var)) {\n            return;\n        }\n\n        $allowed = $reflective\n            ? $var instanceof \\stdClass","sourceCodeStart":355,"sourceCodeEnd":391,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Common/Twig/Extension/GravExtension.php#L355-L391","documentation":"Grav hardens dump-style filters (print_r, vardump, yaml_encode, json_encode, string) when rendering sandboxed Twig content. scanSandboxDump() walks the value before serializing and refuses structures nested deeper than 16 levels, raising SecurityNotAllowedFilterError naming the filter. This catches both pathological/cyclic data that would recurse or hang the serializer and attempts to exfiltrate deep object graphs through a dump filter.","triggerScenarios":"In sandboxed content: {{ page.header|print_r }} where the header nests more than 16 levels; |yaml_encode or |json_encode on deeply nested arrays; structures containing circular references (parent points to child points back to parent), which make the scan depth explode; plugin-built headers embedding entire objects or collections.","commonSituations":"Debug statements (|print_r, |vardump) left in sandboxed page content; page headers built by plugins that embed deep structures; user-supplied YAML with unbounded nesting rendered through a dump filter.","solutions":["Serialize only the fields you need instead of the whole structure: {{ page.header.title }} or {{ page.header.taxonomy.category|join(', ') }}","Reduce the nesting in the source YAML, or sanitize the header in a plugin (onPageContentProcessed or similar) before render","Move genuinely needed deep dumps into a real theme template, which is not sandboxed","If this fires on data you believe is shallow, inspect it for unintended cycles (objects referencing their parents) — that is usually the real bug"],"exampleFix":"{# before, in sandboxed content: deep/cyclic header #}\n{{ page.header|print_r }}\n\n{# after: explicit field access #}\n{{ page.header.title }} — {{ page.header.taxonomy.category|join(', ') }}","handlingStrategy":"try-catch","validationCode":"// pre-check nesting depth (and cycles) before passing a value to dump filters in sandboxed content\nfunction maxNestingDepth(mixed $v, int $depth = 0, array &$seen = []): int\n{\n    if ($depth > 16 || (is_object($v) && isset($seen[spl_object_id($v)]))) { return PHP_INT_MAX; }\n    if (is_object($v)) { $seen[spl_object_id($v)] = true; $values = get_object_vars($v); }\n    elseif (is_array($v)) { $values = $v; } else { return $depth; }\n    $max = $depth;\n    foreach ($values as $child) { $max = max($max, maxNestingDepth($child, $depth + 1, $seen)); }\n    return $max;\n}\n// if (maxNestingDepth($value) > 16) { render a placeholder instead of dumping }","typeGuard":null,"tryCatchPattern":"use Twig\\Sandbox\\SecurityNotAllowedFilterError;\ntry { echo $twig->render($sandboxedTemplate, $data); }\ncatch (SecurityNotAllowedFilterError $e) { log_refused_filter($e->getFilterName()); echo '<!-- dump refused by sandbox -->'; }","preventionTips":["Keep |print_r / |vardump / |json_encode / |yaml_encode / |string out of sandboxed content; use explicit field access","Cap and sanitize page-header nesting in plugins that build headers","Watch for cycles when attaching parent/back-references to header objects"],"tags":["twig","sandbox","security","recursion","serialization"],"backgroundTag":"twig-sandbox-violation","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}