diff --git a/tests/Extension/SandboxTest.php b/tests/Extension/SandboxTest.php index c13f9871c..01e2a242c 100644 --- a/tests/Extension/SandboxTest.php +++ b/tests/Extension/SandboxTest.php @@ -771,6 +771,54 @@ class SandboxTest extends TestCase } } + public function testSandboxAllowedMagicCallMethod(): void + { + $twig = $this->getEnvironment(true, [], ['index' => '{{ o.hello }}'], [], [], [MagicCallObject::class => 'hello']); + $this->assertSame('call:hello', $twig->load('index')->render(['o' => new MagicCallObject()]), 'Sandbox allows a virtual method routed through __call() when its name is allowed'); + } + + public function testSandboxUnallowedMagicCallMethod(): void + { + $twig = $this->getEnvironment(true, [], ['index' => '{{ o.hello }}']); + try { + $twig->load('index')->render(['o' => new MagicCallObject()]); + $this->fail('Sandbox throws a SecurityError exception if a virtual method routed through __call() is not allowed'); + } catch (SecurityNotAllowedPropertyError $e) { + $this->assertEquals(MagicCallObject::class, $e->getClassName()); + $this->assertEquals('hello', $e->getPropertyName()); + } + } + + public function testSandboxUnallowedMagicCallMethodWithMethodSyntax(): void + { + $twig = $this->getEnvironment(true, [], ['index' => '{{ o.hello() }}']); + try { + $twig->load('index')->render(['o' => new MagicCallObject()]); + $this->fail('Sandbox throws a SecurityError exception if a virtual method routed through __call() is not allowed'); + } catch (SecurityNotAllowedMethodError $e) { + $this->assertEquals(MagicCallObject::class, $e->getClassName()); + $this->assertEquals('hello', $e->getMethodName()); + } + } + + public function testSandboxAllowingCallLiteralDoesNotAllowMagicCallMethod(): void + { + $twig = $this->getEnvironment(true, [], ['index' => '{{ o.hello }}'], [], [], [MagicCallObject::class => '__call']); + try { + $twig->load('index')->render(['o' => new MagicCallObject()]); + $this->fail('Sandbox does not allow every virtual method just because "__call" is allowed'); + } catch (SecurityNotAllowedPropertyError $e) { + $this->assertEquals(MagicCallObject::class, $e->getClassName()); + $this->assertEquals('hello', $e->getPropertyName()); + } + } + + public function testSandboxFallsBackToMagicCallMethodForUnallowedProperty(): void + { + $twig = $this->getEnvironment(true, [], ['index' => '{{ o.secret }}'], [], [], [MagicCallObject::class => 'secret']); + $this->assertSame('call:secret', $twig->load('index')->render(['o' => new MagicCallObject()]), 'Sandbox falls back to __call() when a real property is not allowed but the method is'); + } + /** * @dataProvider getSandboxUnallowedToStringTests */ @@ -2560,6 +2608,16 @@ class MagicObject } } +class MagicCallObject +{ + public $secret = 'secret'; + + public function __call($name, $arguments) + { + return 'call:'.$name; + } +} + // Plain object without __toString: column tests exercise property access, not // string coercion, so the array elements must not be Stringable to avoid // triggering the generic filter-input __toString sandbox check.