Deprecate the fact that the parent, block, and attribute functions are always allowed in a sandboxed template

This commit is contained in:
Fabien Potencier
2026-05-24 21:19:08 +02:00
parent d0a5017fd5
commit cfaa2fd030
6 changed files with 132 additions and 2 deletions
+1
View File
@@ -1,5 +1,6 @@
# 3.27.0 (2026-XX-XX)
* 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
* Escape root profile name in `HtmlDumper`
+4
View File
@@ -304,6 +304,10 @@ Sandbox
deprecated as of Twig 3.12. You will need to explicitly allow them if needed
in 4.0.
* 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.
* The ``Twig\Sandbox\SourcePolicyInterface`` interface is deprecated as of Twig
3.27.0 with no replacement. Passing an instance to the
``Twig\Extension\SandboxExtension`` constructor triggers a deprecation.
@@ -54,7 +54,13 @@ final class FunctionExpressionParser extends AbstractExpressionParser implements
$fakeNode = new EmptyNode($line);
$fakeNode->setSourceContext($parser->getStream()->getSourceContext());
return ($function->getParserCallable())($parser, $fakeNode, $args, $line);
$node = ($function->getParserCallable())($parser, $fakeNode, $args, $line);
// remember the original function name so the sandbox can enforce
// the `allowedFunctions` allow-list even though the parser callable
// returned a specialized node (e.g. `parent`, `block`, `attribute`).
$node->setAttribute('sandboxed_function_name', $name);
return $node;
}
if (!isset($this->readyNodes[$class = $function->getNodeClass()])) {
+10
View File
@@ -66,6 +66,16 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
$this->functions[$node->getAttribute('name')] = $node->getTemplateLine();
}
// look for functions whose parser callable replaced the FunctionExpression
// with a specialized node (e.g. `parent`, `block`, `attribute`); the
// original function name was stashed by FunctionExpressionParser.
if ($node->hasAttribute('sandboxed_function_name')) {
$name = $node->getAttribute('sandboxed_function_name');
if (!isset($this->functions[$name])) {
$this->functions[$name] = $node->getTemplateLine();
}
}
// the .. operator is equivalent to the range() function
if ($node instanceof RangeBinary && !isset($this->functions['range'])) {
$this->functions['range'] = $node->getTemplateLine();
+9 -1
View File
@@ -86,7 +86,15 @@ final class SecurityPolicy implements SecurityPolicyInterface
foreach ($functions as $function) {
if (!\in_array($function, $this->allowedFunctions, true)) {
throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function);
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.');
} else {
throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function);
}
}
}
}
+101
View File
@@ -147,6 +147,107 @@ class SandboxTest extends TestCase
yield ['use', '{% use "1_empty" %}'];
}
/**
* @dataProvider getSandboxedForParserCallableFunctionsTests
*
* @group legacy
*/
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));
$twig = $this->getEnvironment(true, [], $extraTemplates, $allowedTags, [], $allowedMethods, $allowedProperties, []);
$this->assertSame($expected, $twig->load($templateName)->render($context));
}
public static function getSandboxedForParserCallableFunctionsTests()
{
yield 'attribute on array' => [
'attribute',
'index',
['index' => '{{ attribute(data, "secret") }}'],
[], [], [],
['data' => ['secret' => 'LEAK']],
'LEAK',
];
yield 'attribute on object property' => [
'attribute',
'index',
['index' => '{{ attribute(obj, "bar") }}'],
[], [], [FooObject::class => ['bar']],
['obj' => new FooObject()],
'bar',
];
yield 'attribute on object method' => [
'attribute',
'index',
['index' => '{{ attribute(obj, "foo") }}'],
[], [FooObject::class => ['foo']], [],
['obj' => new FooObject()],
'foo',
];
yield 'block from same template' => [
'block',
'index',
['index' => '{% block content %}B{% endblock %}{{ block("content") }}'],
['block'], [], [], [],
'BB',
];
yield 'parent inside inherited block' => [
'parent',
'child',
[
'base' => '{% block content %}PARENT{% endblock %}',
'child' => '{% extends "base" %}{% block content %}{{ parent() }} CHILD{% endblock %}',
],
['block'], [], [], [],
'PARENT CHILD',
];
}
/**
* @dataProvider getAllowedParserCallableFunctionsTests
*/
public function testSandboxWithAllowedParserCallableFunctions(string $templateName, array $extraTemplates, array $allowedTags, array $allowedMethods, array $allowedProperties, array $allowedFunctions, array $context, string $expected)
{
$twig = $this->getEnvironment(true, [], $extraTemplates, $allowedTags, [], $allowedMethods, $allowedProperties, $allowedFunctions);
$this->assertSame($expected, $twig->load($templateName)->render($context));
}
public static function getAllowedParserCallableFunctionsTests()
{
yield 'attribute allowed' => [
'index',
['index' => '{{ attribute(data, "x") }}'],
[], [], [], ['attribute'],
['data' => ['x' => 'OK']],
'OK',
];
yield 'block allowed' => [
'index',
['index' => '{% block content %}B{% endblock %}{{ block("content") }}'],
['block'], [], [], ['block'],
[],
'BB',
];
yield 'parent allowed' => [
'child',
[
'base' => '{% block content %}PARENT{% endblock %}',
'child' => '{% extends "base" %}{% block content %}{{ parent() }} CHILD{% endblock %}',
],
['block', 'extends'], [], [], ['parent'],
[],
'PARENT CHILD',
];
}
public function testSandboxWithInheritance()
{
$twig = $this->getEnvironment(true, [], self::$templates, ['extends', 'block']);