diff --git a/src/Extension/SandboxExtension.php b/src/Extension/SandboxExtension.php index dca3262a432..2230810359f 100644 --- a/src/Extension/SandboxExtension.php +++ b/src/Extension/SandboxExtension.php @@ -15,6 +15,7 @@ use Twig\Sandbox\SecurityNotAllowedMethodError; use Twig\Sandbox\SecurityNotAllowedPropertyError; use Twig\Sandbox\SecurityPolicyInterface; +use Twig\Sandbox\SourcePolicyInterface; use Twig\Source; use Twig\TokenParser\SandboxTokenParser; @@ -23,11 +24,13 @@ final class SandboxExtension extends AbstractExtension private $sandboxedGlobally; private $sandboxed; private $policy; + private $sourcePolicy; - public function __construct(SecurityPolicyInterface $policy, $sandboxed = false) + public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, SourcePolicyInterface $sourcePolicy = null) { $this->policy = $policy; $this->sandboxedGlobally = $sandboxed; + $this->sourcePolicy = $sourcePolicy; } public function getTokenParsers() @@ -50,9 +53,9 @@ public function disableSandbox() $this->sandboxed = false; } - public function isSandboxed() + public function isSandboxed(Source $source = null) { - return $this->sandboxedGlobally || $this->sandboxed; + return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source); } public function isSandboxedGlobally() @@ -60,6 +63,15 @@ public function isSandboxedGlobally() return $this->sandboxedGlobally; } + private function isSourceSandboxed(?Source $source): bool + { + if (null === $source || null === $this->sourcePolicy) { + return false; + } + + return $this->sourcePolicy->enableSandbox($source); + } + public function setSecurityPolicy(SecurityPolicyInterface $policy) { $this->policy = $policy; @@ -70,16 +82,16 @@ public function getSecurityPolicy() return $this->policy; } - public function checkSecurity($tags, $filters, $functions) + public function checkSecurity($tags, $filters, $functions, Source $source = null) { - if ($this->isSandboxed()) { + if ($this->isSandboxed($source)) { $this->policy->checkSecurity($tags, $filters, $functions); } } public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $source = null) { - if ($this->isSandboxed()) { + if ($this->isSandboxed($source)) { try { $this->policy->checkMethodAllowed($obj, $method); } catch (SecurityNotAllowedMethodError $e) { @@ -93,7 +105,7 @@ public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $sour public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $source = null) { - if ($this->isSandboxed()) { + if ($this->isSandboxed($source)) { try { $this->policy->checkPropertyAllowed($obj, $property); } catch (SecurityNotAllowedPropertyError $e) { @@ -107,7 +119,7 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $ public function ensureToStringAllowed($obj, int $lineno = -1, Source $source = null) { - if ($this->isSandboxed() && \is_object($obj) && method_exists($obj, '__toString')) { + if ($this->isSandboxed($source) && \is_object($obj) && method_exists($obj, '__toString')) { try { $this->policy->checkMethodAllowed($obj, '__toString'); } catch (SecurityNotAllowedMethodError $e) { diff --git a/src/Node/CheckSecurityNode.php b/src/Node/CheckSecurityNode.php index 489a3652ddf..7b2981bbe53 100644 --- a/src/Node/CheckSecurityNode.php +++ b/src/Node/CheckSecurityNode.php @@ -58,7 +58,8 @@ public function compile(Compiler $compiler) ->indent() ->write(!$tags ? "[],\n" : "['".implode("', '", array_keys($tags))."'],\n") ->write(!$filters ? "[],\n" : "['".implode("', '", array_keys($filters))."'],\n") - ->write(!$functions ? "[]\n" : "['".implode("', '", array_keys($functions))."']\n") + ->write(!$functions ? "[],\n" : "['".implode("', '", array_keys($functions))."'],\n") + ->write("\$this->source\n") ->outdent() ->write(");\n") ->outdent() diff --git a/src/Sandbox/SourcePolicyInterface.php b/src/Sandbox/SourcePolicyInterface.php new file mode 100644 index 00000000000..b952f1ea6fa --- /dev/null +++ b/src/Sandbox/SourcePolicyInterface.php @@ -0,0 +1,24 @@ +load('1_childobj_childmethod')->render(self::$params); } catch (SecurityError $e) { $this->fail('checkMethodAllowed is exiting prematurely after matching a parent class and not seeing a method allowed on a child class later in the list'); - } + } try { $twig_child_first->load('1_childobj_parentmethod')->render(self::$params); @@ -449,15 +450,50 @@ public function testMultipleClassMatchesViaInheritanceInAllowedMethods() } } - protected function getEnvironment($sandboxed, $options, $templates, $tags = [], $filters = [], $methods = [], $properties = [], $functions = []) + protected function getEnvironment($sandboxed, $options, $templates, $tags = [], $filters = [], $methods = [], $properties = [], $functions = [], $sourcePolicy = null) { $loader = new ArrayLoader($templates); $twig = new Environment($loader, array_merge(['debug' => true, 'cache' => false, 'autoescape' => false], $options)); $policy = new SecurityPolicy($tags, $filters, $methods, $properties, $functions); - $twig->addExtension(new SandboxExtension($policy, $sandboxed)); + $twig->addExtension(new SandboxExtension($policy, $sandboxed, $sourcePolicy)); return $twig; } + + public function testSandboxSourcePolicyEnableReturningFalse() + { + $twig = $this->getEnvironment(false, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface { + public function enableSandbox(Source $source): bool + { + return '1_basic' != $source->getName(); + } + }); + $this->assertEquals('FOO', $twig->load('1_basic')->render(self::$params)); + } + + public function testSandboxSourcePolicyEnableReturningTrue() + { + $twig = $this->getEnvironment(false, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface { + public function enableSandbox(Source $source): bool + { + return '1_basic' === $source->getName(); + } + }); + $this->expectException(SecurityError::class); + $twig->load('1_basic')->render([]); + } + + public function testSandboxSourcePolicyFalseDoesntOverrideOtherEnables() + { + $twig = $this->getEnvironment(true, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface { + public function enableSandbox(Source $source): bool + { + return false; + } + }); + $this->expectException(SecurityError::class); + $twig->load('1_basic')->render([]); + } } class ParentClass