mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-21 17:41:41 +00:00
Guard sandbox __toString walker against self-referencing iterables
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
* Fix sandbox bypass in the "column" filter when sandboxing is enabled via `SourcePolicyInterface`
|
||||
* Fix sandbox `__toString` bypass via `Traversable` arguments to the `join` and `replace` filters (also covers containers that implement both `Stringable` and `Traversable`)
|
||||
* Fix sandbox `__toString` bypass via the `in` and `not in` operators
|
||||
* Prevent a stack overflow in `SandboxExtension::ensureToStringAllowed()` when a self-referencing iterable is passed to a sandboxed template
|
||||
|
||||
# 3.26.0 (2026-05-20)
|
||||
|
||||
|
||||
@@ -125,9 +125,34 @@ final class SandboxExtension extends AbstractExtension
|
||||
* @throws SecurityNotAllowedMethodError
|
||||
*/
|
||||
public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null)
|
||||
{
|
||||
return $this->doEnsureToStringAllowed($obj, $lineno, $source, new \SplObjectStorage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialises a spread operand and runs the policy on every element.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @throws SecurityNotAllowedMethodError
|
||||
*/
|
||||
public function ensureSpreadAllowed(iterable $obj, int $lineno = -1, ?Source $source = null): array
|
||||
{
|
||||
$seen = new \SplObjectStorage();
|
||||
if ($obj instanceof \Traversable) {
|
||||
$seen[$obj] = true;
|
||||
$obj = iterator_to_array($obj);
|
||||
}
|
||||
|
||||
$this->ensureToStringAllowedForArray($obj, $lineno, $source, $seen);
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
private function doEnsureToStringAllowed($obj, int $lineno, ?Source $source, \SplObjectStorage $seen)
|
||||
{
|
||||
if (\is_array($obj)) {
|
||||
$this->ensureToStringAllowedForArray($obj, $lineno, $source);
|
||||
$this->ensureToStringAllowedForArray($obj, $lineno, $source, $seen);
|
||||
|
||||
return $obj;
|
||||
}
|
||||
@@ -154,8 +179,17 @@ final class SandboxExtension extends AbstractExtension
|
||||
// the `__toString` check above only validates the container's own coercion,
|
||||
// not the elements yielded by `getIterator()`.
|
||||
if ($obj instanceof \Traversable) {
|
||||
// Guard against self-referencing iterables (e.g. an IteratorAggregate
|
||||
// whose getIterator() yields $this): without this check, materialising
|
||||
// and recursing into the elements would overflow the stack. Mirrors
|
||||
// the array-cycle guard in ensureToStringAllowedForArray().
|
||||
if (isset($seen[$obj])) {
|
||||
return $obj;
|
||||
}
|
||||
|
||||
$seen[$obj] = true;
|
||||
$array = iterator_to_array($obj);
|
||||
$this->ensureToStringAllowedForArray($array, $lineno, $source);
|
||||
$this->ensureToStringAllowedForArray($array, $lineno, $source, $seen);
|
||||
|
||||
// Return the materialised array only when the object is not also
|
||||
// Stringable, so that callers that rely on `__toString` (e.g. `{{ obj }}`)
|
||||
@@ -169,25 +203,7 @@ final class SandboxExtension extends AbstractExtension
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialises a spread operand and runs the policy on every element.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @throws SecurityNotAllowedMethodError
|
||||
*/
|
||||
public function ensureSpreadAllowed(iterable $obj, int $lineno = -1, ?Source $source = null): array
|
||||
{
|
||||
if ($obj instanceof \Traversable) {
|
||||
$obj = iterator_to_array($obj);
|
||||
}
|
||||
|
||||
$this->ensureToStringAllowedForArray($obj, $lineno, $source);
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
private function ensureToStringAllowedForArray(array $obj, int $lineno, ?Source $source, array &$stack = []): void
|
||||
private function ensureToStringAllowedForArray(array $obj, int $lineno, ?Source $source, \SplObjectStorage $seen, array &$stack = []): void
|
||||
{
|
||||
foreach ($obj as $k => $v) {
|
||||
if (!$v) {
|
||||
@@ -195,7 +211,7 @@ final class SandboxExtension extends AbstractExtension
|
||||
}
|
||||
|
||||
if (!\is_array($v)) {
|
||||
$this->ensureToStringAllowed($v, $lineno, $source);
|
||||
$this->doEnsureToStringAllowed($v, $lineno, $source, $seen);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -207,7 +223,7 @@ final class SandboxExtension extends AbstractExtension
|
||||
$stack[$r->getId()] = true;
|
||||
}
|
||||
|
||||
$this->ensureToStringAllowedForArray($v, $lineno, $source, $stack);
|
||||
$this->ensureToStringAllowedForArray($v, $lineno, $source, $seen, $stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1260,6 +1260,35 @@ EOF
|
||||
$this->assertSame('stringable-traversable', $twig->load('index')->render($params));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getCyclicTraversableTemplates
|
||||
*/
|
||||
public function testSandboxHandlesCyclicTraversableWithoutStackOverflow(string $template)
|
||||
{
|
||||
// A self-referencing IteratorAggregate must not cause the sandbox policy
|
||||
// walker to recurse infinitely when materialising the iterable. PHP itself
|
||||
// throws a clean error when the cyclic object reaches `implode()` /
|
||||
// string coercion; the sandbox must NOT turn that into a stack overflow.
|
||||
$twig = $this->getEnvironment(
|
||||
true,
|
||||
[],
|
||||
['index' => $template],
|
||||
[],
|
||||
['join', 'replace'],
|
||||
);
|
||||
|
||||
$this->expectException(RuntimeError::class);
|
||||
|
||||
$twig->load('index')->render(['obj' => new CyclicTraversableObject()]);
|
||||
}
|
||||
|
||||
public static function getCyclicTraversableTemplates(): iterable
|
||||
{
|
||||
yield 'join' => ['{{ obj|join(",") }}'];
|
||||
yield 'replace' => ['{{ "x"|replace(obj) }}'];
|
||||
yield 'spread' => ['{{ ["a", ...obj]|join(",") }}'];
|
||||
}
|
||||
|
||||
public function testSourcePolicySandboxBlocksToStringInTraversableJoin()
|
||||
{
|
||||
$sourcePolicy = new class implements SourcePolicyInterface {
|
||||
@@ -1827,3 +1856,15 @@ class StringableTraversableObject implements \IteratorAggregate, \Stringable
|
||||
yield from $this->items;
|
||||
}
|
||||
}
|
||||
|
||||
// Self-referencing IteratorAggregate: getIterator() yields `$this`. Used to
|
||||
// verify that the sandbox policy walker (which materialises Traversables to
|
||||
// enforce the `__toString` policy on yielded elements) does not recurse
|
||||
// infinitely.
|
||||
class CyclicTraversableObject implements \IteratorAggregate
|
||||
{
|
||||
public function getIterator(): \Traversable
|
||||
{
|
||||
yield $this;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user