Merge branch '3.x' into 4.x

* 3.x:
  Clarify the security scope for untrusted templates
  Normalize destructuring assignment targets
  Deprecate duplicate macro definitions

# Conflicts:
#	CHANGELOG
#	doc/deprecated.rst
#	tests/ExpressionParserTest.php
This commit is contained in:
Fabien Potencier
2026-07-28 09:58:56 +02:00
8 changed files with 100 additions and 4 deletions
+31
View File
@@ -0,0 +1,31 @@
Security Policy
===============
DO NOT PUBLISH SECURITY REPORTS PUBLICLY.
Reporting a Security Issue
--------------------------
If you find an issue that might have security implications, send a report to
security[at]symfony.com.
The full [security reporting and resolution process][1] is described in the
Symfony documentation.
Security Scope for Untrusted Templates
--------------------------------------
Twig treats template source as trusted code unless the template is rendered in
the [Twig sandbox][2]. The regular Twig environment is not a security boundary.
Applications that render templates supplied by untrusted users must enable and
correctly configure the Twig sandbox. Any behavior that is possible because an
application renders an untrusted template without the sandbox is not a security
issue in Twig and must not be reported as one.
Reports about untrusted templates are in scope only when they demonstrate a
sandbox restriction bypass while the sandbox is enabled and its security policy
does not allow the demonstrated operation.
[1]: https://symfony.com/security
[2]: https://twig.symfony.com/doc/3.x/sandbox.html
+8
View File
@@ -3,6 +3,14 @@ Twig Sandbox
The ``sandbox`` extension can be used to evaluate untrusted code.
.. warning::
Twig treats template source as trusted code by default. If an application
accepts templates from untrusted users, it must enable and correctly
configure the sandbox. The regular Twig environment is not a security
boundary, and any behavior caused by rendering an untrusted template
without the sandbox is not a security issue in Twig.
Registering the Sandbox
-----------------------
@@ -19,6 +19,7 @@ use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Binary\ObjectDestructuringSetBinary;
use Twig\Node\Expression\Binary\SequenceDestructuringSetBinary;
use Twig\Node\Expression\Binary\SetBinary;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Parser;
use Twig\Token;
@@ -49,6 +50,12 @@ class AssignmentExpressionParser extends BinaryOperatorExpressionParser
};
if ($left instanceof ArrayExpression) {
foreach ($left->getKeyValuePairs() as $i => $pair) {
if ($pair['value'] instanceof ContextVariable && !$pair['value'] instanceof AssignContextVariable) {
$left->setNode(2 * $i + 1, new AssignContextVariable($pair['value']->getAttribute('name'), $pair['value']->getTemplateLine()));
}
}
if ($left->isSequence()) {
return new SequenceDestructuringSetBinary($left, $right, $token->getLine());
}
@@ -16,7 +16,7 @@ use Twig\Error\SyntaxError;
use Twig\Extension\SandboxExtension;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Node;
/**
@@ -37,7 +37,7 @@ class ObjectDestructuringSetBinary extends AbstractBinary
throw new \LogicException('Left side must be ArrayExpression for object/mapping destructuring.');
}
foreach ($left->getKeyValuePairs() as $pair) {
if (!$pair['value'] instanceof ContextVariable) {
if (!$pair['value'] instanceof AssignContextVariable) {
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned in object/mapping destructuring.', $pair['value']::class), $lineno);
}
@@ -16,7 +16,7 @@ use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\EmptyExpression;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Node;
/**
@@ -35,7 +35,7 @@ class SequenceDestructuringSetBinary extends AbstractBinary
foreach ($left->getKeyValuePairs() as $pair) {
if ($pair['value'] instanceof EmptyExpression) {
$this->variables[] = null;
} elseif ($pair['value'] instanceof ContextVariable) {
} elseif ($pair['value'] instanceof AssignContextVariable) {
$this->variables[] = $pair['value']->getAttribute('name');
} else {
throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned in sequence destructuring.', $pair['value']::class), $lineno);
+4
View File
@@ -284,6 +284,10 @@ class Parser
public function setMacro(string $name, MacroNode $node): void
{
if (isset($this->macros[$name])) {
trigger_deprecation('twig/twig', '3.29', 'Defining the macro "%s" more than once in "%s" is deprecated and will throw a SyntaxError in Twig 4.0 (first definition at line %d, second at line %d).', $name, $this->stream->getSourceContext()->getName(), $this->macros[$name]->getTemplateLine(), $node->getTemplateLine());
}
$this->macros[$name] = $node;
}
+33
View File
@@ -34,9 +34,13 @@ use Twig\Extension\AbstractExtension;
use Twig\Loader\ArrayLoader;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\Binary\ObjectDestructuringSetBinary;
use Twig\Node\Expression\Binary\SequenceDestructuringSetBinary;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\EmptyExpression;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\Node\Expression\Unary\SpreadUnary;
use Twig\Node\Expression\Variable\AssignContextVariable;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Parser;
use Twig\Source;
@@ -231,6 +235,35 @@ class ExpressionParserTest extends TestCase
$env->compileSource(new Source('{{ [1,,2] }}', 'index'));
}
public function testSequenceDestructuringUsesAssignmentTargets(): void
{
$env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]);
$parser = new Parser($env);
$node = $parser->parse($env->tokenize(new Source('{{ ([first, , third] = values) }}', 'index')))->getNode('body')->getNode('0')->getNode('expr');
$this->assertInstanceOf(SequenceDestructuringSetBinary::class, $node);
$pairs = $node->getNode('left')->getKeyValuePairs();
$this->assertSame(AssignContextVariable::class, $pairs[0]['value']::class);
$this->assertSame('first', $pairs[0]['value']->getAttribute('name'));
$this->assertSame(EmptyExpression::class, $pairs[1]['value']::class);
$this->assertSame(AssignContextVariable::class, $pairs[2]['value']::class);
$this->assertSame('third', $pairs[2]['value']->getAttribute('name'));
}
public function testObjectDestructuringUsesAssignmentTargets(): void
{
$env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]);
$parser = new Parser($env);
$node = $parser->parse($env->tokenize(new Source('{{ ({name: user_name} = user) }}', 'index')))->getNode('body')->getNode('0')->getNode('expr');
$this->assertInstanceOf(ObjectDestructuringSetBinary::class, $node);
$pair = $node->getNode('left')->getKeyValuePairs()[0];
$this->assertSame(ConstantExpression::class, $pair['key']::class);
$this->assertSame('name', $pair['key']->getAttribute('value'));
$this->assertSame(AssignContextVariable::class, $pair['value']::class);
$this->assertSame('user_name', $pair['value']->getAttribute('name'));
}
#[DataProvider('getTestsForString')]
public function testStringExpression($template, $expected): void
{
@@ -0,0 +1,13 @@
--TEST--
Defining a macro more than once is deprecated
--DEPRECATION--
Since twig/twig 3.29: Defining the macro "greet" more than once in "index.twig" is deprecated and will throw a SyntaxError in Twig 4.0 (first definition at line 3, second at line 4).
--TEMPLATE--
{% import _self as macros %}
{% macro greet() %}first{% endmacro %}
{% macro greet() %}second{% endmacro %}
{{ macros.greet() }}
--DATA--
return []
--EXPECT--
second