The sandbox is the part of Twig you reach for when you let people you don’t
trust write templates: a CMS where users edit their own pages, an email
builder, a notification system driven by customer-provided snippets. You hand
Twig a security policy listing the tags, filters, functions, methods, and
properties you are willing to expose, and everything else is rejected.
That promise only holds if the policy actually covers everything a template
can do. For a long time, it didn’t. Twig 4.0 closes the gaps, and most of the
work is already available on the 3.x branch so you can adopt it today.
The Problem: A Policy That Didn’t Mean What It Said
Take a policy that looks airtight. You list a handful of tags and filters, no
functions, no extra tests, and you feel safe:
$policy = new SecurityPolicy(
allowedTags: ['if', 'for'],
allowedFilters: ['escape', 'upper'],
allowedMethods: ['Article' => ['getTitle', 'getBody']],
allowedProperties: [],
allowedFunctions: [],
);
Now a template author writes this:
{# tests were never checked, so this ran unchecked #}
{% if 'SOME_SECRET' is constant('SOME_SECRET') %}...{% endif %}
{# functions you never listed, allowed anyway #}
{{ attribute(article, 'getSecret') }}
None of these are in your policy. All of them ran. The sandbox ignored tests
entirely, so any test, including constant and any custom one, went through
unchecked; and a short list of tags and functions (extends, use,
parent, block, attribute) was hardcoded as always allowed
regardless of what your policy said.
Nobody likes security tools that are lenient by default; a policy you have to
second-guess is worse than no policy, because it gives you false confidence.
Twig 4.0 makes the allow-list mean exactly what it says.
Tests Are Now Part of the Sandbox
Tests (the is operator: is even, is defined, is constant(...))
are now a first-class allow-list, alongside tags, filters, and functions. The
SecurityPolicy constructor gained a sixth argument for them:
$policy = new SecurityPolicy(
allowedTags: ['if', 'for'],
allowedFilters: ['escape', 'upper'],
allowedMethods: ['Article' => ['getTitle', 'getBody']],
allowedProperties: [],
allowedFunctions: [],
allowedTests: ['prime'],
);
A custom test that is not listed is now rejected like anything else:
{% if number is prime %}...{% endif %}
{# TwigSandboxSecurityNotAllowedTestError:
Test "prime" is not allowed in "page" at line 1. #}
You don’t need to enumerate the safe built-ins, though. The harmless ones
(defined, empty, even, odd, iterable, same as,
null, divisible by and the rest) are flagged as always allowed, so
templates relying on them keep working without a single line of configuration.
The one exception is constant, which reaches into the PHP runtime: it must
be allow-listed explicitly, like a custom test.
No More Silent Exceptions
The tags and functions that used to be allowed behind your back, extends,
use, parent, block, attribute, and the constant test, are
no longer special. In 4.0 they obey the policy like everything else: list them
if your templates need them, and they are rejected if you don’t.
This is the kind of change that needs a preview before a major release, so you
can opt into the 4.0 behavior today on 3.x with a single call:
$policy->setStrict(true);
In strict mode, calling one of these without allow-listing it fails right away:
{{ attribute(article, 'getSecret') }}
{# TwigSandboxSecurityNotAllowedFunctionError:
Function "attribute" is not allowed in "page" at line 1. #}
Turn strict mode on in your test suite, fix what it flags, and your policy is
ready for 4.0.
Marking Inherently-Safe Items as Always Allowed
Tightening the default raises a fair question: must every application now
re-list dozens of harmless filters like upper or trim? No.
Twig 4.0 introduces a way for the author of a callable to declare it safe for
any sandbox, so no policy has to opt into it. Set the
always_allowed_in_sandbox option on a filter, function, or test:
$twig->addFilter(new TwigFilter('upper', 'strtoupper', [
'always_allowed_in_sandbox' => true,
]));
For a tag, override isAlwaysAllowedInSandbox() on its token parser:
final class MyTagTokenParser extends AbstractTokenParser
{
public function isAlwaysAllowedInSandbox(): bool
{
return true;
}
// ...
}
A marked item skips the sandbox check entirely, so it costs nothing at runtime
and never needs to appear in an allow-list. This is a sharp tool, so the
documentation spells out the criteria an item must meet to deserve the flag:
no new capability, no PHP runtime access, no callable arguments, no template
resolution, no output-safety bypass, deterministic output, and a few more. If
in doubt, leave the flag off and let the policy decide.
Built-ins Allowed Out of the Box
Twig applies that same flag to its own toolbox. A curated set of built-ins
that meet the criteria, the pure value transformations and control-flow tags
you use in every template, are always allowed in 4.0:
- Tags:
apply,block,do,for,guard,if,macro,
set,types,with. - Filters:
abs,batch,capitalize,convert_encoding,
default,e,escape,first,format,join,keys,
last,length,lower,merge,nl2br,number_format,
replace,reverse,round,slice,split,striptags,
title,trim,upper,url_encode. - Functions:
cycle,max,min.
Note what is not on the list: map, filter, sort and friends (they
take a callable you may want to forbid), include, source and the other
template-resolving helpers, random and shuffle (non-deterministic),
json_encode and dump (object introspection), constant (PHP runtime
access). Those stay under your policy, where they belong.
When you upgrade, you can delete the safe built-ins from your allow-lists;
listing a name that is always allowed has no effect, so there is no rush.
The Upgrade Path
As usual, Twig 3.x triggers a deprecation for everything that will break in
4.0. All the fixes work on 3.x, so an application that runs deprecation-free,
or that already enables strict mode on its security policy, is ready for the
new sandbox.




