Skip to content

Commit

Permalink
Merge branch '2.x' into 3.x
Browse files Browse the repository at this point in the history
* 2.x:
  Add SourcePolicyInterface to selectively enable the Sandbox based on a template's Source
  • Loading branch information
fabpot committed Dec 19, 2023
2 parents d2f493f + a4974b2 commit 204cd6f
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 11 deletions.
30 changes: 23 additions & 7 deletions src/Extension/SandboxExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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(): array
Expand All @@ -50,16 +53,25 @@ public function disableSandbox(): void
$this->sandboxed = false;
}

public function isSandboxed(): bool
public function isSandboxed(Source $source = null): bool
{
return $this->sandboxedGlobally || $this->sandboxed;
return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source);
}

public function isSandboxedGlobally(): bool
{
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;
Expand All @@ -70,16 +82,20 @@ public function getSecurityPolicy(): SecurityPolicyInterface
return $this->policy;
}

<<<<<<< HEAD
public function checkSecurity($tags, $filters, $functions): void
=======
public function checkSecurity($tags, $filters, $functions, Source $source = null)
>>>>>>> 2.x
{
if ($this->isSandboxed()) {
if ($this->isSandboxed($source)) {
$this->policy->checkSecurity($tags, $filters, $functions);
}
}

public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $source = null): void
{
if ($this->isSandboxed()) {
if ($this->isSandboxed($source)) {
try {
$this->policy->checkMethodAllowed($obj, $method);
} catch (SecurityNotAllowedMethodError $e) {
Expand All @@ -93,7 +109,7 @@ public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $sour

public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $source = null): void
{
if ($this->isSandboxed()) {
if ($this->isSandboxed($source)) {
try {
$this->policy->checkPropertyAllowed($obj, $property);
} catch (SecurityNotAllowedPropertyError $e) {
Expand All @@ -107,7 +123,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) {
Expand Down
3 changes: 2 additions & 1 deletion src/Node/CheckSecurityNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ public function compile(Compiler $compiler): void
->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()
Expand Down
24 changes: 24 additions & 0 deletions src/Sandbox/SourcePolicyInterface.php
Original file line number Diff line number Diff line change
@@ -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;
}
42 changes: 39 additions & 3 deletions tests/Extension/SandboxTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Sandbox\SecurityNotAllowedTagError;
use Twig\Sandbox\SecurityPolicy;
use Twig\Source;

class SandboxTest extends TestCase
{
Expand Down Expand Up @@ -440,7 +441,7 @@ public function testMultipleClassMatchesViaInheritanceInAllowedMethods()
$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);
Expand All @@ -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
Expand Down

0 comments on commit 204cd6f

Please sign in to comment.