Fix sandbox bypass in the "column" filter under SourcePolicyInterface

This commit is contained in:
Fabien Potencier
2026-05-22 10:06:46 +02:00
parent 3feda8a850
commit 09c6706407
3 changed files with 85 additions and 3 deletions
+1
View File
@@ -6,6 +6,7 @@
* Restrict allowed classes in `Twig\Profiler\Profile::unserialize()` to prevent arbitrary class instantiation
* 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`
# 3.26.0 (2026-05-20)
+6 -3
View File
@@ -1960,12 +1960,15 @@ final class CoreExtension extends AbstractExtension
}
if ($isSandboxed) {
$sandbox = $env->getExtension(SandboxExtension::class);
// The sandbox might be enabled via a SourcePolicyInterface, in which case the SandboxExtension
// would not consider the sandbox active without the current Source: $isSandboxed is already
// computed against the call-site source, so check the policy directly to honor that decision.
$policy = $env->getExtension(SandboxExtension::class)->getSecurityPolicy();
foreach ($array as $item) {
if (\is_object($item)) {
$sandbox->checkPropertyAllowed($item, (string) $name);
$policy->checkPropertyAllowed($item, (string) $name);
if (null !== $index) {
$sandbox->checkPropertyAllowed($item, (string) $index);
$policy->checkPropertyAllowed($item, (string) $index);
}
}
}
+78
View File
@@ -1173,6 +1173,84 @@ EOF
$this->assertSame('bar', $twig->load('index')->render($params));
}
public function testSourcePolicySandboxBlocksColumnFilterOnDisallowedProperty()
{
$sourcePolicy = new class implements SourcePolicyInterface {
public function enableSandbox(Source $source): bool
{
return true;
}
};
$params = ['obj' => new ColumnObject()];
$twig = $this->getEnvironment(false, [], ['index' => "{{ [obj]|column('bar')|first }}"], [], ['column', 'first'], [], [], [], $sourcePolicy);
try {
$twig->load('index')->render($params);
$this->fail('Sandbox should reject the "column" filter when the requested property is not in allowedProperties (SourcePolicyInterface).');
} catch (SecurityNotAllowedPropertyError $e) {
$this->assertSame('Twig\Tests\Extension\ColumnObject', $e->getClassName());
$this->assertSame('bar', $e->getPropertyName());
}
}
public function testSourcePolicySandboxBlocksColumnFilterOnDisallowedIndex()
{
$sourcePolicy = new class implements SourcePolicyInterface {
public function enableSandbox(Source $source): bool
{
return true;
}
};
$params = ['obj' => new ColumnObject()];
$twig = $this->getEnvironment(false, [], ['index' => "{{ [obj]|column('bar', 'foo')|keys|first }}"], [], ['column', 'first', 'keys'], [], ['Twig\Tests\Extension\ColumnObject' => ['bar']], [], $sourcePolicy);
try {
$twig->load('index')->render($params);
$this->fail('Sandbox should reject the "column" filter when the index argument targets a disallowed property (SourcePolicyInterface).');
} catch (SecurityNotAllowedPropertyError $e) {
$this->assertSame('Twig\Tests\Extension\ColumnObject', $e->getClassName());
$this->assertSame('foo', $e->getPropertyName());
}
}
public function testSourcePolicySandboxAllowsColumnFilterOnAllowedProperty()
{
$sourcePolicy = new class implements SourcePolicyInterface {
public function enableSandbox(Source $source): bool
{
return true;
}
};
$params = ['obj' => new ColumnObject()];
$twig = $this->getEnvironment(false, [], ['index' => "{{ [obj]|column('bar')|first }}"], [], ['column', 'first'], [], ['Twig\Tests\Extension\ColumnObject' => ['bar']], [], $sourcePolicy);
$this->assertSame('bar', $twig->load('index')->render($params));
}
public function testSourcePolicySandboxBlocksColumnFilterOnMagicGetter()
{
$sourcePolicy = new class implements SourcePolicyInterface {
public function enableSandbox(Source $source): bool
{
return true;
}
};
$params = ['magic' => new MagicObject()];
$twig = $this->getEnvironment(false, [], ['index' => "{{ [magic]|column('anything')|first }}"], [], ['column', 'first'], [], [], [], $sourcePolicy);
try {
$twig->load('index')->render($params);
$this->fail('Sandbox should reject the "column" filter before invoking __get on a non-allowlisted property (SourcePolicyInterface).');
} catch (SecurityNotAllowedPropertyError $e) {
$this->assertSame('Twig\Tests\Extension\MagicObject', $e->getClassName());
$this->assertSame('anything', $e->getPropertyName());
}
}
protected function getEnvironment($sandboxed, $options, $templates, $tags = [], $filters = [], $methods = [], $properties = [], $functions = [], $sourcePolicy = null, bool $strict = false)
{
$loader = new ArrayLoader($templates);