Fix sandbox __toString bypass via the in and not in operators

This commit is contained in:
Fabien Potencier
2026-05-24 08:29:59 +02:00
parent cc1e21a2a2
commit 8d6af0707b
4 changed files with 21 additions and 2 deletions
+1
View File
@@ -8,6 +8,7 @@
* 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
* Fix sandbox `__toString` bypass via the `in` and `not in` operators
# 3.26.0 (2026-05-20)
+7 -1
View File
@@ -12,9 +12,10 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\CoercesChildrenToStringInterface;
use Twig\Node\Expression\ReturnBoolInterface;
class InBinary extends AbstractBinary implements ReturnBoolInterface
class InBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
{
public function compile(Compiler $compiler): void
{
@@ -31,4 +32,9 @@ class InBinary extends AbstractBinary implements ReturnBoolInterface
{
return $compiler->raw('in');
}
public function getStringCoercedChildNames(): array
{
return ['left', 'right'];
}
}
+7 -1
View File
@@ -12,9 +12,10 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Node\CoercesChildrenToStringInterface;
use Twig\Node\Expression\ReturnBoolInterface;
class NotInBinary extends AbstractBinary implements ReturnBoolInterface
class NotInBinary extends AbstractBinary implements ReturnBoolInterface, CoercesChildrenToStringInterface
{
public function compile(Compiler $compiler): void
{
@@ -31,4 +32,9 @@ class NotInBinary extends AbstractBinary implements ReturnBoolInterface
{
return $compiler->raw('not in');
}
public function getStringCoercedChildNames(): array
{
return ['left', 'right'];
}
}
+6
View File
@@ -614,6 +614,12 @@ class SandboxTest extends TestCase
'concat_right_in_if' => ['{% if "" ~ obj %}LEAK{% endif %}'],
'range_left' => ['{% for x in obj..1 %}LEAK{% endfor %}'],
'range_right' => ['{% for x in 1..obj %}LEAK{% endfor %}'],
'in_array_right' => ['{% if "needle" in [obj] %}LEAK{% endif %}'],
'in_array_left' => ['{% if obj in ["needle"] %}LEAK{% endif %}'],
'notin_array_right' => ['{% if "needle" not in [obj] %}LEAK{% endif %}'],
'notin_array_left' => ['{% if obj not in ["needle"] %}LEAK{% endif %}'],
'in_iterator_right' => ['{% if "needle" in iterator %}LEAK{% endif %}'],
'notin_iterator_right' => ['{% if "needle" not in iterator %}LEAK{% endif %}'],
'do_tag_function_arg' => ['{% do my_func(obj) %}'],
'do_tag_filter_input' => ['{% do obj|upper %}'],
'do_tag_concat' => ['{% do obj ~ "" %}'],