mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-22 18:31:41 +00:00
Add a strict mode to SecurityPolicy to opt-in to the 4.0 sandbox behavior for the extends/use tags and the parent/block/attribute functions
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# 3.27.0 (2026-XX-XX)
|
||||
|
||||
* Add a strict mode to `Twig\Sandbox\SecurityPolicy` to opt-in to the 4.0 behavior for the `extends`/`use` tags and the `parent`/`block`/`attribute` functions, which are otherwise still implicitly allowed in a sandbox
|
||||
* Deprecate the fact that the `parent`, `block`, and `attribute` functions are always allowed in a sandboxed template
|
||||
* Fix PHP 8.1+ implicit float-to-int deprecation triggered by sandboxed `ArrayAccess` attribute access with a float key
|
||||
* Restrict allowed classes in `Twig\Profiler\Profile::unserialize()` to prevent arbitrary class instantiation
|
||||
|
||||
+6
-2
@@ -302,11 +302,15 @@ Sandbox
|
||||
|
||||
* Having the ``extends`` and ``use`` tags allowed by default in a sandbox is
|
||||
deprecated as of Twig 3.12. You will need to explicitly allow them if needed
|
||||
in 4.0.
|
||||
in 4.0. To opt-in to the 4.0 behavior now (so the tags need to be
|
||||
allow-listed or get rejected), enable strict mode on the security policy by
|
||||
calling ``$policy->setStrict(true)``.
|
||||
|
||||
* Having the ``parent``, ``block``, and ``attribute`` functions allowed by
|
||||
default in a sandbox is deprecated as of Twig 3.27. You will need to
|
||||
explicitly allow them if needed in 4.0.
|
||||
explicitly allow them if needed in 4.0. The same ``setStrict(true)`` toggle
|
||||
on ``Twig\Sandbox\SecurityPolicy`` opts-in to the 4.0 behavior for these
|
||||
functions too.
|
||||
|
||||
* The ``Twig\Sandbox\SourcePolicyInterface`` interface is deprecated as of Twig
|
||||
3.27.0 with no replacement. Passing an instance to the
|
||||
|
||||
+8
-3
@@ -48,9 +48,14 @@ allowed and will generate a ``\Twig\Sandbox\SecurityError`` exception.
|
||||
|
||||
.. caution::
|
||||
|
||||
The ``extends`` and ``use`` tags are always allowed in a sandboxed
|
||||
template. That behavior will change in 4.0 where these tags will need to be
|
||||
explicitly allowed like any other tag.
|
||||
The ``extends`` and ``use`` tags, as well as the ``parent``, ``block``, and
|
||||
``attribute`` functions are always allowed in a sandboxed template. That
|
||||
behavior will change in 4.0 where they will need to be explicitly allowed
|
||||
like any other tag or function. To opt-in to the 4.0 behavior now (so they
|
||||
need to be allow-listed or get rejected), enable strict mode on the
|
||||
security policy::
|
||||
|
||||
$policy->setStrict(true);
|
||||
|
||||
Enabling the Sandbox
|
||||
--------------------
|
||||
|
||||
@@ -26,6 +26,7 @@ final class SecurityPolicy implements SecurityPolicyInterface
|
||||
private $allowedMethods;
|
||||
private $allowedProperties;
|
||||
private $allowedFunctions;
|
||||
private bool $strict = false;
|
||||
|
||||
public function __construct(array $allowedTags = [], array $allowedFilters = [], array $allowedMethods = [], array $allowedProperties = [], array $allowedFunctions = [])
|
||||
{
|
||||
@@ -64,14 +65,28 @@ final class SecurityPolicy implements SecurityPolicyInterface
|
||||
$this->allowedFunctions = $functions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles strict mode.
|
||||
*
|
||||
* In strict mode, the tags and functions that are historically always allowed in a
|
||||
* sandbox (the ``extends`` and ``use`` tags, the ``parent``, ``block``, and
|
||||
* ``attribute`` functions) are no longer implicitly allowed and must be added to the
|
||||
* relevant allow-list to be usable. Use this flag in 3.x to opt-in to the forthcoming
|
||||
* 4.0 behavior and silence the related deprecations.
|
||||
*/
|
||||
public function setStrict(bool $strict): void
|
||||
{
|
||||
$this->strict = $strict;
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
foreach ($tags as $tag) {
|
||||
if (!\in_array($tag, $this->allowedTags, true)) {
|
||||
if ('extends' === $tag) {
|
||||
trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.');
|
||||
} elseif ('use' === $tag) {
|
||||
trigger_deprecation('twig/twig', '3.12', 'The "use" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.');
|
||||
if (!$this->strict && 'extends' === $tag) {
|
||||
trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
|
||||
} elseif (!$this->strict && 'use' === $tag) {
|
||||
trigger_deprecation('twig/twig', '3.12', 'The "use" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
|
||||
} else {
|
||||
throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag);
|
||||
}
|
||||
@@ -86,12 +101,12 @@ final class SecurityPolicy implements SecurityPolicyInterface
|
||||
|
||||
foreach ($functions as $function) {
|
||||
if (!\in_array($function, $this->allowedFunctions, true)) {
|
||||
if ('parent' === $function) {
|
||||
trigger_deprecation('twig/twig', '3.27', 'The "parent" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.');
|
||||
} elseif ('block' === $function) {
|
||||
trigger_deprecation('twig/twig', '3.27', 'The "block" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.');
|
||||
} elseif ('attribute' === $function) {
|
||||
trigger_deprecation('twig/twig', '3.27', 'The "attribute" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.');
|
||||
if (!$this->strict && 'parent' === $function) {
|
||||
trigger_deprecation('twig/twig', '3.27', 'The "parent" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
|
||||
} elseif (!$this->strict && 'block' === $function) {
|
||||
trigger_deprecation('twig/twig', '3.27', 'The "block" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
|
||||
} elseif (!$this->strict && 'attribute' === $function) {
|
||||
trigger_deprecation('twig/twig', '3.27', 'The "attribute" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
|
||||
} else {
|
||||
throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function);
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ class SandboxTest extends TestCase
|
||||
*/
|
||||
public function testSandboxForExtendsAndUseTags(string $tag, string $template)
|
||||
{
|
||||
$this->expectDeprecation(\sprintf('Since twig/twig 3.12: The "%s" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.', $tag));
|
||||
$this->expectDeprecation(\sprintf('Since twig/twig 3.12: The "%s" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).', $tag));
|
||||
|
||||
$twig = $this->getEnvironment(true, [], self::$templates, []);
|
||||
$twig->createTemplate($template, 'index')->render([]);
|
||||
@@ -154,7 +154,7 @@ class SandboxTest extends TestCase
|
||||
*/
|
||||
public function testSandboxForParserCallableFunctions(string $function, string $templateName, array $extraTemplates, array $allowedTags, array $allowedMethods, array $allowedProperties, array $context, string $expected)
|
||||
{
|
||||
$this->expectDeprecation(\sprintf('Since twig/twig 3.27: The "%s" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.', $function));
|
||||
$this->expectDeprecation(\sprintf('Since twig/twig 3.27: The "%s" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).', $function));
|
||||
|
||||
$twig = $this->getEnvironment(true, [], $extraTemplates, $allowedTags, [], $allowedMethods, $allowedProperties, []);
|
||||
$this->assertSame($expected, $twig->load($templateName)->render($context));
|
||||
@@ -248,6 +248,98 @@ class SandboxTest extends TestCase
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getStrictSandboxRejectsGrandfatheredTagsTests
|
||||
*/
|
||||
public function testStrictSandboxRejectsGrandfatheredTags(string $tag, string $template)
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], self::$templates, [], [], [], [], [], null, true);
|
||||
|
||||
$this->expectException(SecurityNotAllowedTagError::class);
|
||||
$this->expectExceptionMessage(\sprintf('Tag "%s" is not allowed', $tag));
|
||||
|
||||
$twig->createTemplate($template, 'index')->render([]);
|
||||
}
|
||||
|
||||
public static function getStrictSandboxRejectsGrandfatheredTagsTests()
|
||||
{
|
||||
yield ['extends', '{% extends "1_empty" %}'];
|
||||
yield ['use', '{% use "1_empty" %}'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getStrictSandboxRejectsGrandfatheredFunctionsTests
|
||||
*/
|
||||
public function testStrictSandboxRejectsGrandfatheredFunctions(string $function, string $templateName, array $extraTemplates, array $allowedTags, array $context)
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], $extraTemplates, $allowedTags, [], [], [], [], null, true);
|
||||
|
||||
$this->expectException(SecurityNotAllowedFunctionError::class);
|
||||
$this->expectExceptionMessage(\sprintf('Function "%s" is not allowed', $function));
|
||||
|
||||
$twig->load($templateName)->render($context);
|
||||
}
|
||||
|
||||
public static function getStrictSandboxRejectsGrandfatheredFunctionsTests()
|
||||
{
|
||||
yield 'attribute' => [
|
||||
'attribute',
|
||||
'index',
|
||||
['index' => '{{ attribute(data, "secret") }}'],
|
||||
[],
|
||||
['data' => ['secret' => 'LEAK']],
|
||||
];
|
||||
|
||||
yield 'block' => [
|
||||
'block',
|
||||
'index',
|
||||
['index' => '{% block content %}B{% endblock %}{{ block("content") }}'],
|
||||
['block'],
|
||||
[],
|
||||
];
|
||||
|
||||
yield 'parent' => [
|
||||
'parent',
|
||||
'child',
|
||||
[
|
||||
'base' => '{% block content %}PARENT{% endblock %}',
|
||||
'child' => '{% extends "base" %}{% block content %}{{ parent() }} CHILD{% endblock %}',
|
||||
],
|
||||
['extends', 'block'],
|
||||
[],
|
||||
];
|
||||
}
|
||||
|
||||
public function testStrictSandboxStillAllowsExplicitlyAllowedGrandfatheredNames()
|
||||
{
|
||||
$twig = $this->getEnvironment(
|
||||
true,
|
||||
[],
|
||||
[
|
||||
'base' => '{% block content %}PARENT{% endblock %}',
|
||||
'child' => '{% extends "base" %}{% block content %}{{ parent() }} CHILD - {{ attribute(data, "x") }}{% endblock %}',
|
||||
],
|
||||
['extends', 'block'],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
['parent', 'attribute'],
|
||||
null,
|
||||
true,
|
||||
);
|
||||
|
||||
$this->assertSame('PARENT CHILD - OK', $twig->load('child')->render(['data' => ['x' => 'OK']]));
|
||||
}
|
||||
|
||||
public function testStrictModeCanBeEnabledViaSetterAfterConstruction()
|
||||
{
|
||||
$policy = new SecurityPolicy([], [], [], [], []);
|
||||
$policy->setStrict(true);
|
||||
|
||||
$this->expectException(SecurityNotAllowedTagError::class);
|
||||
$policy->checkSecurity(['extends'], [], []);
|
||||
}
|
||||
|
||||
public function testSandboxWithInheritance()
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], self::$templates, ['extends', 'block']);
|
||||
@@ -1081,11 +1173,12 @@ EOF
|
||||
$this->assertSame('bar', $twig->load('index')->render($params));
|
||||
}
|
||||
|
||||
protected function getEnvironment($sandboxed, $options, $templates, $tags = [], $filters = [], $methods = [], $properties = [], $functions = [], $sourcePolicy = null)
|
||||
protected function getEnvironment($sandboxed, $options, $templates, $tags = [], $filters = [], $methods = [], $properties = [], $functions = [], $sourcePolicy = null, bool $strict = false)
|
||||
{
|
||||
$loader = new ArrayLoader($templates);
|
||||
$twig = new Environment($loader, array_merge(['debug' => true, 'cache' => false, 'autoescape' => false], $options));
|
||||
$policy = new SecurityPolicy($tags, $filters, $methods, $properties, $functions);
|
||||
$policy->setStrict($strict);
|
||||
$twig->addExtension(new SandboxExtension($policy, $sandboxed, $sourcePolicy));
|
||||
|
||||
return $twig;
|
||||
|
||||
Reference in New Issue
Block a user