mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-20 01:02:51 +00:00
Fix inconsistent array access with a Stringable key
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user