Twig 3.26.0 went out as a security release fixing thirteen advisories (two critical, three high, four medium, four low) on 2026-05-20 per Fabien Potencier’s walkthrough on the Symfony blog and the v3.26.0 tag on GitHub (Twig 3.26.0 released, GitHub release v3.26.0). The tag timestamp is 07:32 UTC that day, which is the sort of artifact you want when your change board asks “did this land before or after our nightly build?”
If you only skim headlines, do not flatten this into “another XSS.” Two issues let a template author steer compiled cache PHP; several others punch through the sandbox policy in ways that look boring in a diff but hurt in production when you let strangers or tenants ship Twig.
Critical: PHP in the compiled cache file
The Symfony write-up separates two compiler paths that both ended in arbitrary PHP inside the generated cache file, executed the first time the template rendered.
Macro name abuse (_self.(…)): when the receiver is _self (or an {% import %} alias) and the parenthesized bit is a string literal, the parser could short-circuit into the macro-call path and concatenate attacker-controlled text into a macro reference without validating it as an identifier. The compiler then emitted that name raw into generated PHP (CVE-2026-46640, GHSA-45vw-wh46-2vx8).
Single quotes in {% use %} names: Compiler::string() escaped characters needed inside double-quoted PHP string literals but not single quotes. A malicious template name passed through {% use %} could terminate a surrounding single-quoted PHP literal early and slip arbitrary PHP into the cache file. The fix encodes single quotes (for example as \x27) so the generated PHP cannot break out that way (CVE-2026-46633, GHSA-7p85-w9px-jpjp).
If you host multi-tenant Twig, CMS fields that compile to templates, or anything that treats templates as code, treat these two like remote code execution in your threat model until you are on 3.26.0+.
High: sandbox policy that quietly turned off
Object destructuring: Twig 3.24.0 added object-destructuring assignment that compiled to CoreExtension::getAttribute() with $sandboxed hardcoded false, so property and method allow-lists stopped applying for those reads (CVE-2026-46639).
__toString() coercions: a long list of node types could coerce Stringable children to strings without the sandbox visitor wrapping them, so __toString() on objects in context became an unguarded exfiltration and side-effect channel (CVE-2026-47732, GHSA-pr2w-4gpj-cpq4). The fix introduces CoercesChildrenToStringInterface so nodes declare which children need checks, plus tighter handling for spread args and dynamic attribute names.
Source-policy sandboxing: when sandboxing is driven by SourcePolicyInterface, arrow callbacks in sort / filter / map / reduce could skip the policy check because checkArrow() did not receive the current Source (CVE-2026-24425).
Medium and low still matter for integrators
Skim the full list in the Symfony post if you ship extras or profilers: the column filter used native array_column() on objects, which reads properties in C without going through CoreExtension::getAttribute(), bypassing the sandbox allow-list (CVE-2026-46635). The {% sandbox %}{% include %} path could skip checkSecurity() on templates already cached from a non-sandbox load, i.e. an incomplete follow-on to the older CVE-2024-45411 story (CVE-2026-46638, GHSA-7fxw-r6jv-74c8). template_from_string() plus selective sandboxing could render inner templates with checkSecurity() effectively neutered because synthesized names do not match policy hooks (CVE-2026-46634).
On the “looks like XSS but teaches a lesson” side, the deprecated spaceless filter was registered is_safe => ['html'], so autoescape did not re-wrap its output; piping user-shaped markup through spaceless could emit raw HTML even when the author never wrote |raw (CVE-2026-46628, standalone advisory). The Symfony post notes downstream forks (they call out Drupal modules as an example) that copied the filter and inherited the same is_safe mistake; worth grepping your own tree, not only Composer’s copy of twig/twig.
Extras (cssinliner, inky, markdown) had is_safe annotations that did not match reality; IntlExtension had unbounded formatter memoisation keyed on template-controlled arguments (CVE-2026-46637, CVE-2026-46629). HtmlDumper in the profiler omitted escaping on template and profile names (CVE-2026-47730).
What to do in the next maintenance window
- Bump
twig/twigto 3.26.0 (and anytwig/*-extrapackages the Symfony post lists) incomposer.json, then run your usual test pass; this is not a silent doc-only bump. - Grep for
|spaceless,{% spaceless %}, and any copied spaceless implementation in app or module code. - If you use the sandbox, re-read the new documentation callouts: resource exhaustion is explicitly out of scope for the sandbox guarantees (CVE-2026-46627), and
template_from_string()plus source policies needs a hard look if you thought “sandboxed include” was enough boundary.
Twig’s own release page on GitHub mirrors the CVE list in commit subjects; use it when you need a short changelog link for CI annotations (v3.26.0 compare).