mirror of
https://github.com/twigphp/Twig.git
synced 2026-08-28 19:17:37 +00:00
Add SourcePolicyInterface to selectively enable the Sandbox based on a template's Source
This commit is contained in:
committed by
Fabien Potencier
parent
02262dee80
commit
a18da1614a
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user