Fabien Potencier
Contributed by Fabien Potencier in #4813 , #4816 , #4817 and #4819

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:

1
2
3
4
5
6
7
$policy = new SecurityPolicy(
    allowedTags: ['if', 'for'],
    allowedFilters: ['escape', 'upper'],
    allowedMethods: ['Article' => ['getTitle', 'getBody']],
    allowedProperties: [],
    allowedFunctions: [],
);

Now a template author writes this:

1
2
3
4
5
{# 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:

1
2
3
4
5
6
7
8
$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:

1
2
3
{% if number is prime %}...{% endif %}
{# Twig\Sandbox\SecurityNotAllowedTestError:
   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:

1
$policy->setStrict(true);

In strict mode, calling one of these without allow-listing it fails right away:

1
2
3
{{ attribute(article, 'getSecret') }}
{# Twig\Sandbox\SecurityNotAllowedFunctionError:
   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:

1
2
3
$twig->addFilter(new TwigFilter('upper', 'strtoupper', [
    'always_allowed_in_sandbox' => true,
]));

For a tag, override isAlwaysAllowedInSandbox() on its token parser:

1
2
3
4
5
6
7
8
9
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.

Published in #Living on the edge #Twig