{"id":"CVE-2026-49981","title":"Twig: Sandbox filter, tag and function allow-list bypass when sandbox state changes between renders for a cached `Template`","summary":"Twig: Sandbox filter, tag and function allow-list bypass when sandbox state changes between renders for a cached `Template`","severity":"high","cwe":["CWE-693","CWE-863"],"vendor":"twig","product":"twig/twig","ecosystem":"composer","affected":["twig/twig <= 3.26.0"],"patched":["twig/twig 3.27.0"],"published":"2026-07-01","updated":"2026-07-01","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-529h-vh3j-85hq","references":[{"url":"https://github.com/twigphp/Twig/security/advisories/GHSA-529h-vh3j-85hq"},{"url":"https://github.com/twigphp/Twig/releases/tag/v3.27.0"},{"url":"https://github.com/advisories/GHSA-529h-vh3j-85hq"}],"tags":["ghsa","composer"],"ingestedAt":"2026-07-01T19:15:59.908Z","epss":0.00362,"epssPercentile":0.30003,"slug":"CVE-2026-49981","body":"## Overview\n\n### Description\n\nThe per-template filter, tag and function allow-list check is compiled into the `checkSecurity()` method of each `Template` subclass and was invoked once from the constructor, gated by `SandboxExtension::isSandboxed($source)`. `Template` instances are then cached on the `Environment` in `$loadedTemplates`, so the verdict computed at construction time was sticky for the rest of the process.\n\nAny later change of sandbox state on the same `Environment` left that cached verdict in place: toggling `SandboxExtension::enableSandbox()`/`disableSandbox()`, swapping the policy via `setSecurityPolicy()`, a `SourcePolicyInterface` decision flip, or simply having a parent, macro or included template pre-instantiated outside the sandbox before a sandboxed render reached it. In all of these cases, the filters, tags and functions used by the affected template kept running with the original (typically empty) check, bypassing the `SecurityPolicy` allow-list.\n\nMethod, property and `__toString` allow-lists are not affected: they are enforced at every call site at runtime through `SandboxExtension::checkMethodAllowed()`, `checkPropertyAllowed()` and `ensureToStringAllowed()`, which re-read the current state on every call.\n\nLong-lived workers (FrankenPHP, RoadRunner, Symfony Messenger consumers, FPM with hot autoloading) that share a single `Environment` between sandboxed and non-sandboxed renders are the most exposed: a single non-sandboxed render of a shared layout pre-warms its `Template` instance, after which any later sandboxed render that extends, uses, includes or imports from that layout silently skips the filter/tag/function allow-list for the pre-warmed instance.\n\n### Resolution\n\nThe allow-list check is no longer run from the constructor. `Template` gains a public `ensureSecurityChecked()` method that calls the compiled `checkSecurity()` only when `SandboxExtension::isSandboxed($source)` returns true for the current source, and it is invoked at every entry point that can reach a `Template` instance whose security has not yet been verified against the current state: `Template::yield()`, `Template::yieldBlock()` (on the resolved block template, which covers `extends`, `use`, traits and parent blocks), `Template::getParent()` (which evaluates user code when the parent name is dynamic) and `Template::getTemplateForMacro()` (on the resolved macro template).\n\nThe explicit `checkSecurity()` calls previously emitted by `IncludeNode` and `CoreExtension::include()` are removed: the included template's own `yield()` now re-runs the check against the current sandbox state. The compiled `checkSecurity()` body is a cheap walk over compile-time-static arrays, so the per-render cost is negligible. Old cached compiled PHP files keep working unchanged: the constructor-time call they still contain is idempotent.\n\n### Credits\n\nTwig would like to thank Fabien Potencier for reporting and fixing the issue.\n\n## Affected packages\n\n- `twig/twig <= 3.26.0`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `twig/twig 3.27.0`","depth":"twilight","depthScore":41,"depthScoreParts":{"impact":41.3,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}