Twig 3.27.0 ships five sandbox security fixes (one low, four medium) and a
new opt-in strict mode for SecurityPolicy that lets you adopt the
forthcoming 4.0 sandbox defaults today. We recommend upgrading any
application that renders user-controlled templates in a sandbox; details
and CVE references are below so you can assess your exposure.
Sandbox: opt into 4.0 strict mode today
In a sandbox, the extends and use tags as well as the parent,
block, and attribute functions have always been implicitly allowed,
regardless of what your SecurityPolicy declared. Twig 3.12 started
emitting a deprecation for the two tags; this release extends it to the
three functions, because 4.0 will require all of them to be listed
explicitly.
To preview the 4.0 behavior on 3.x (and silence the deprecations), enable
strict mode on SecurityPolicy:
1 2 3 4 5 6 7 8
use Twig\Sandbox\SecurityPolicy;
$policy = new SecurityPolicy(
allowedTags: ['extends', 'if', 'for'],
allowedFilters: ['escape', 'upper'],
allowedFunctions: ['parent', 'block'],
);
$policy->setStrict(true);
With strict mode on, every tag and every function must appear in the
relevant allow-list to be usable. Any template that relied on the
historical implicit allowance will throw a SecurityNotAllowedTagError
or SecurityNotAllowedFunctionError, which is exactly what 4.0 will do.
Profiler: harden HtmlDumper and Profile::unserialize()
Two hardening fixes land in the profiler. HtmlDumper now escapes the
root profile name, so a profile created with a hostile name no longer
emits raw HTML into the dump output. And Profile::unserialize() now
restricts the classes PHP is allowed to instantiate to Profile itself,
preventing arbitrary __unserialize() / __wakeup() magic methods
from running when an untrusted profile blob is deserialized.
SourcePolicyInterface is deprecated
Twig\Sandbox\SourcePolicyInterface is now deprecated with no
replacement. The feature sees no real-world use, and the recommended way
to render templates written by untrusted authors is to point a dedicated
sandboxed Environment at a loader that restricts what those templates
can see, rather than toggling sandbox state per source from within a
shared environment.
Security fixes
This release fixes five sandbox vulnerabilities. All of them are policy
enforcement gaps rather than direct code execution issues; the realistic
impact is unauthorized disclosure of data exposed through __toString()
methods or property allow-lists. Applications that do not render
sandboxed templates are unaffected.
Medium
A cached Template instance kept the filter, tag, and function
allow-list verdict computed at construction time, so any later change of
sandbox state on the same Environment was ignored. Long-lived workers
(FrankenPHP, RoadRunner, Symfony Messenger consumers) sharing a single
Environment between sandboxed and non-sandboxed renders are the most
exposed: a non-sandboxed render of a shared layout pre-warmed its
instance, after which any sandboxed render that extended, included, or
imported from that layout silently skipped the allow-list. The check now
runs at every entry point and is re-evaluated against the current sandbox
state (CVE-2026-46636).
A dynamic mapping key such as {% set arr = {(obj): "value"} %}
emitted a raw (string) cast on the key expression, so PHP invoked
__toString() on the resolved object without ever consulting the
sandbox policy. ArrayExpression now declares its dynamic keys as
string-coercion sites, and as a side effect any expression (not only
context variables) is accepted as a dynamic mapping key
(CVE-2026-48806).
The join and replace filters, and the in / not in
operators, coerced Stringable objects to strings without going
through ensureToStringAllowed(). Both filters materialize
Traversable arguments through implode() / strtr(); the
operators fall through to PHP's <=> comparison. The fix recurses
into Traversable operands when computing the policy check, and wraps
both sides of in / not in so the policy is consulted before any
coercion happens
(CVE-2026-48807).
When sandboxing was enabled through SourcePolicyInterface, the
column filter routed property reads through SandboxExtension with
a null Source, which short-circuited the per-source decision and
skipped the property allow-list entirely. The filter now calls the
security policy directly using the sandbox state already computed at the
call site
(CVE-2026-48808).
Low
The deprecated internal wrappers in src/Resources/core.php
(twig_check_arrow_in_sandbox(), twig_array_some(),
twig_array_every()) were not updated when 3.26.0 changed the
underlying CoreExtension signatures to take an explicit
$isSandboxed boolean. As a result, array.some and array.every
silently dropped the Closure-only restriction when invoked through
the wrappers, and twig_check_arrow_in_sandbox() threw a TypeError
on PHP 8+. The wrappers now resolve the sandbox state through the same
helper that compiled templates use. Compiled templates themselves were
never affected
(CVE-2026-48805).
Credits
Thanks to El Kharoubi Iosif for reporting CVE-2026-48805 and CVE-2026-48806, Vincent55 Yang for reporting CVE-2026-48808 and co-reporting CVE-2026-48807, and Fabien Potencier for reporting CVE-2026-46636, co-reporting CVE-2026-48807, and providing the fixes for all five advisories.
Full Changelog
- CVE-2026-46636 Fix sandbox filter/tag/function allow-list bypass when sandbox state changes between renders
- CVE-2026-48805 Fix sandbox bypass in deprecated internal wrappers
- CVE-2026-48806 Fix sandbox
__toStringpolicy bypass via dynamic mapping keys - CVE-2026-48807 Fix sandbox
__toStringbypasses viaTraversableinjoin/replacefilters and thein/not inoperators - CVE-2026-48808 Fix sandbox bypass in the
columnfilter underSourcePolicyInterface - #4817 Add a strict mode to
SecurityPolicyto opt-in to the 4.0 sandbox behavior for theextends/usetags and theparent/block/attributefunctions (@fabpot) - #4813 Deprecate the fact that the
parent,block, andattributefunctions are always allowed in a sandboxed template (@fabpot) - #4812 Fix PHP 8.1+ implicit float-to-int deprecation in sandboxed array access (@fabpot)
- #4807 Escape root profile name in
HtmlDumper(@fabpot) - #4808 Restrict allowed classes in
Profile::unserialize()(@fabpot) - #4803 Deprecate the
Twig\Sandbox\SourcePolicyInterfaceinterface (@fabpot)