Add SourcePolicyInterface to selectively enable the Sandbox based on a template's Source

This commit is contained in:
Yaakov Saxon
2023-09-14 12:11:37 -04:00
committed by Fabien Potencier
parent 02262dee80
commit a18da1614a
4 changed files with 85 additions and 12 deletions
+20 -8
View File
@@ -15,6 +15,7 @@ use Twig\NodeVisitor\SandboxNodeVisitor;
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 @@ final class SandboxExtension extends AbstractExtension
$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 @@ final class SandboxExtension extends AbstractExtension
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 @@ final class SandboxExtension extends AbstractExtension
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 @@ final class SandboxExtension extends AbstractExtension
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 @@ final class SandboxExtension extends AbstractExtension
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) {
+2 -1
View File
@@ -58,7 +58,8 @@ class CheckSecurityNode extends Node
->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()
+24
View File
@@ -0,0 +1,24 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Sandbox;
use Twig\Source;
/**
* Interface for a class that can optionally enable the sandbox mode based on a template's Twig\Source.
*
* @author Yaakov Saxon
*/
interface SourcePolicyInterface
{
public function enableSandbox(Source $source): bool;
}
+39 -3
View File
@@ -24,6 +24,7 @@ use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Sandbox\SecurityNotAllowedTagError;
use Twig\Sandbox\SecurityPolicy;
use Twig\Source;
class SandboxTest extends TestCase
{
@@ -440,7 +441,7 @@ EOF
$twig_parent_first->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 @@ EOF
}
}
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