Fix inconsistent array access with a Stringable key

This commit is contained in:
Fabien Potencier
2026-05-29 09:47:56 +02:00
parent dfb5232d51
commit 8ec9530732
3 changed files with 92 additions and 6 deletions
+1
View File
@@ -1,5 +1,6 @@
# 3.27.1 (2026-XX-XX)
* Fix array access with a `Stringable` key to coerce the key to string consistently instead of throwing in the optimized path
* Fix sandbox replacing `IteratorAggregate` arguments (e.g. Symfony's `FormView`) by a plain array
# 3.27.0 (2026-05-27)
+39 -6
View File
@@ -73,9 +73,9 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
->raw(' instanceof ArrayAccess ? (')
->raw($var)
->raw('[')
->subcompile($this->getNode('attribute'))
->raw('] ?? null) : null)')
;
$this->compileArrayKey($compiler);
$compiler->raw('] ?? null) : null)');
return;
}
@@ -90,9 +90,9 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
->raw(', CoreExtension::ARRAY_LIKE_CLASSES, true) ? (')
->raw($var)
->raw('[')
->subcompile($this->getNode('attribute'))
->raw('] ?? null) : ')
;
$this->compileArrayKey($compiler);
$compiler->raw('] ?? null) : ');
}
if ($this->getAttribute('ignore_strict_check')) {
@@ -160,8 +160,41 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest
public function getStringCoercedChildNames(): array
{
// for a method-like access, the host PHP method may coerce any of its arguments to string
return $this->hasNode('arguments') ? ['arguments'] : [];
$names = [];
// the host PHP method may coerce any argument to string
if ($this->hasNode('arguments')) {
$names[] = 'arguments';
}
// compileArrayKey() coerces a Stringable key; expose it so the sandbox checks __toString()
if (Template::ARRAY_CALL === $this->getAttribute('type')) {
$names[] = 'attribute';
}
return $names;
}
/**
* Coerces a Stringable array key to string so the optimized path matches
* CoreExtension::getAttribute(); scalars are left to PHP's native offset coercion.
*/
private function compileArrayKey(Compiler $compiler): void
{
$attribute = $this->getNode('attribute');
if ($attribute instanceof ConstantExpression) {
$compiler->subcompile($attribute);
return;
}
$key = '$'.$compiler->getVarName();
$compiler
->raw('(('.$key.' = ')
->subcompile($attribute)
->raw(') instanceof \Stringable ? (string) '.$key.' : '.$key.')')
;
}
private function changeIgnoreStrictCheck(self $node): void
+52
View File
@@ -252,6 +252,58 @@ class TemplateTest extends TestCase
$this->assertSame('EmptyString', CoreExtension::getAttribute($twig, $template->getSourceContext(), $array, null), 'null is treated as "" when accessing a sequence/mapping (equals PHP behavior)');
}
/**
* @dataProvider getStrictVariablesModes
*/
public function testArrayAccessWithStringableKeyIsConsistentAcrossStrictModes(bool $strict)
{
$twig = new Environment(new ArrayLoader(['index' => '{{ array[object] }}']), [
'strict_variables' => $strict,
'autoescape' => false,
]);
$object = new class implements \Stringable {
public function __toString(): string
{
return 'string';
}
};
$this->assertSame('value', $twig->render('index', ['array' => ['string' => 'value'], 'object' => $object]));
}
public static function getStrictVariablesModes(): iterable
{
yield 'lax' => [false];
yield 'strict' => [true];
}
public function testArrayAccessWithStringableKeyIsCheckedBySandbox()
{
$object = new class implements \Stringable {
public function __toString(): string
{
return 'string';
}
};
$data = ['array' => ['string' => 'value'], 'object' => $object];
$twig = new Environment(new ArrayLoader(['index' => '{{ array[object] }}']), ['autoescape' => false]);
$twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [], [], []), true));
try {
$twig->render('index', $data);
$this->fail('The sandbox must reject the __toString() coercion of the array key.');
} catch (SecurityError $e) {
$this->assertStringContainsStringIgnoringCase('__toString', $e->getMessage());
}
$twig = new Environment(new ArrayLoader(['index' => '{{ array[object] }}']), ['autoescape' => false]);
$twig->addExtension(new SandboxExtension(new SecurityPolicy([], [], [$object::class => ['__toString']], [], []), true));
$this->assertSame('value', $twig->render('index', $data));
}
/**
* @dataProvider getGetAttributeTests
*/