mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-24 00:36:26 +00:00
Fix sandbox __toString bypass via Traversable in join/replace filters
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
* Escape root profile name in `HtmlDumper`
|
||||
* Deprecate the `Twig\Sandbox\SourcePolicyInterface` interface with no replacement
|
||||
* 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
|
||||
|
||||
# 3.26.0 (2026-05-20)
|
||||
|
||||
|
||||
@@ -132,6 +132,16 @@ final class SandboxExtension extends AbstractExtension
|
||||
return $obj;
|
||||
}
|
||||
|
||||
// A non-Stringable Traversable would later be materialised (e.g. by filters such as
|
||||
// `join` or `replace`) and its elements coerced to string by PHP itself, bypassing
|
||||
// the policy. Materialise it now and recursively check the contents.
|
||||
if ($obj instanceof \Traversable && !$obj instanceof \Stringable && $this->isSandboxed($source)) {
|
||||
$obj = iterator_to_array($obj);
|
||||
$this->ensureToStringAllowedForArray($obj, $lineno, $source);
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
if ($obj instanceof \Stringable && $this->isSandboxed($source)) {
|
||||
try {
|
||||
$this->policy->checkMethodAllowed($obj, '__toString');
|
||||
|
||||
@@ -62,6 +62,8 @@ class SandboxTest extends TestCase
|
||||
'magic' => new MagicObject(),
|
||||
'recursion' => [4],
|
||||
'iterator' => new \ArrayIterator(['a', new FooObject()]),
|
||||
'iterator_map' => new \ArrayIterator(['__toString' => new FooObject()]),
|
||||
'iterator_nested' => new \ArrayIterator(['a', new \ArrayIterator(['b', new FooObject()])]),
|
||||
];
|
||||
self::$params['recursion'][] = &self::$params['recursion'];
|
||||
self::$params['recursion'][] = new FooObject();
|
||||
@@ -589,6 +591,9 @@ class SandboxTest extends TestCase
|
||||
'spread_array_operator' => ['{{ [1, 2, ...[5, 6, 7, obj]]|join(",") }}'],
|
||||
'spread_array_operator_var' => ['{{ [1, 2, ...some_array]|join(",") }}'],
|
||||
'spread_iterator_in_function_args' => ['{{ ["x", ...iterator]|join(",") }}'],
|
||||
'iterator_in_join' => ['{{ iterator|join(", ") }}'],
|
||||
'iterator_nested_in_join' => ['{{ iterator_nested|join(", ") }}'],
|
||||
'iterator_in_replace' => ['{{ "__toString"|replace(iterator_map) }}'],
|
||||
'recursion' => ['{{ recursion|join(", ") }}'],
|
||||
'ternary_print' => ['{{ true ? obj : "" }}'],
|
||||
'ternary_filter_input' => ['{{ (true ? obj : "")|upper }}'],
|
||||
@@ -1165,6 +1170,46 @@ EOF
|
||||
}
|
||||
}
|
||||
|
||||
public function testSourcePolicySandboxBlocksToStringInTraversableJoin()
|
||||
{
|
||||
$sourcePolicy = new class implements SourcePolicyInterface {
|
||||
public function enableSandbox(Source $source): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
$twig = $this->getEnvironment(false, [], ['index' => '{{ iterator|join(", ") }}'], [], ['join'], [], [], [], $sourcePolicy);
|
||||
|
||||
try {
|
||||
$twig->load('index')->render(self::$params);
|
||||
$this->fail('Sandbox should block __toString on objects contained in a Traversable passed to the "join" filter (SourcePolicyInterface).');
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
$this->assertSame('Twig\Tests\Extension\FooObject', $e->getClassName());
|
||||
$this->assertSame('__tostring', $e->getMethodName());
|
||||
}
|
||||
}
|
||||
|
||||
public function testSourcePolicySandboxBlocksToStringInTraversableReplace()
|
||||
{
|
||||
$sourcePolicy = new class implements SourcePolicyInterface {
|
||||
public function enableSandbox(Source $source): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
$twig = $this->getEnvironment(false, [], ['index' => '{{ "__toString"|replace(iterator_map) }}'], [], ['replace'], [], [], [], $sourcePolicy);
|
||||
|
||||
try {
|
||||
$twig->load('index')->render(self::$params);
|
||||
$this->fail('Sandbox should block __toString on objects contained in a Traversable passed to the "replace" filter (SourcePolicyInterface).');
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
$this->assertSame('Twig\Tests\Extension\FooObject', $e->getClassName());
|
||||
$this->assertSame('__tostring', $e->getMethodName());
|
||||
}
|
||||
}
|
||||
|
||||
public function testColumnFilterUnaffectedOutsideSandbox()
|
||||
{
|
||||
$params = ['obj' => new ColumnObject()];
|
||||
|
||||
Reference in New Issue
Block a user