mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-06 15:37:58 +00:00
Deprecate the fact that the parent, block, and attribute functions are always allowed in a sandboxed template
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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()])) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user