Skip to content

Commit a18da16

Browse files
YSaxonfabpot
authored andcommitted
Add SourcePolicyInterface to selectively enable the Sandbox based on a template's Source
1 parent 02262de commit a18da16

File tree

4 files changed

+85
-12
lines changed

4 files changed

+85
-12
lines changed

src/Extension/SandboxExtension.php

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Twig\Sandbox\SecurityNotAllowedMethodError;
1616
use Twig\Sandbox\SecurityNotAllowedPropertyError;
1717
use Twig\Sandbox\SecurityPolicyInterface;
18+
use Twig\Sandbox\SourcePolicyInterface;
1819
use Twig\Source;
1920
use Twig\TokenParser\SandboxTokenParser;
2021

@@ -23,11 +24,13 @@ final class SandboxExtension extends AbstractExtension
2324
private $sandboxedGlobally;
2425
private $sandboxed;
2526
private $policy;
27+
private $sourcePolicy;
2628

27-
public function __construct(SecurityPolicyInterface $policy, $sandboxed = false)
29+
public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, SourcePolicyInterface $sourcePolicy = null)
2830
{
2931
$this->policy = $policy;
3032
$this->sandboxedGlobally = $sandboxed;
33+
$this->sourcePolicy = $sourcePolicy;
3134
}
3235

3336
public function getTokenParsers()
@@ -50,16 +53,25 @@ public function disableSandbox()
5053
$this->sandboxed = false;
5154
}
5255

53-
public function isSandboxed()
56+
public function isSandboxed(Source $source = null)
5457
{
55-
return $this->sandboxedGlobally || $this->sandboxed;
58+
return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source);
5659
}
5760

5861
public function isSandboxedGlobally()
5962
{
6063
return $this->sandboxedGlobally;
6164
}
6265

66+
private function isSourceSandboxed(?Source $source): bool
67+
{
68+
if (null === $source || null === $this->sourcePolicy) {
69+
return false;
70+
}
71+
72+
return $this->sourcePolicy->enableSandbox($source);
73+
}
74+
6375
public function setSecurityPolicy(SecurityPolicyInterface $policy)
6476
{
6577
$this->policy = $policy;
@@ -70,16 +82,16 @@ public function getSecurityPolicy()
7082
return $this->policy;
7183
}
7284

73-
public function checkSecurity($tags, $filters, $functions)
85+
public function checkSecurity($tags, $filters, $functions, Source $source = null)
7486
{
75-
if ($this->isSandboxed()) {
87+
if ($this->isSandboxed($source)) {
7688
$this->policy->checkSecurity($tags, $filters, $functions);
7789
}
7890
}
7991

8092
public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $source = null)
8193
{
82-
if ($this->isSandboxed()) {
94+
if ($this->isSandboxed($source)) {
8395
try {
8496
$this->policy->checkMethodAllowed($obj, $method);
8597
} catch (SecurityNotAllowedMethodError $e) {
@@ -93,7 +105,7 @@ public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $sour
93105

94106
public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $source = null)
95107
{
96-
if ($this->isSandboxed()) {
108+
if ($this->isSandboxed($source)) {
97109
try {
98110
$this->policy->checkPropertyAllowed($obj, $property);
99111
} catch (SecurityNotAllowedPropertyError $e) {
@@ -107,7 +119,7 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $
107119

108120
public function ensureToStringAllowed($obj, int $lineno = -1, Source $source = null)
109121
{
110-
if ($this->isSandboxed() && \is_object($obj) && method_exists($obj, '__toString')) {
122+
if ($this->isSandboxed($source) && \is_object($obj) && method_exists($obj, '__toString')) {
111123
try {
112124
$this->policy->checkMethodAllowed($obj, '__toString');
113125
} catch (SecurityNotAllowedMethodError $e) {

src/Node/CheckSecurityNode.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ public function compile(Compiler $compiler)
5858
->indent()
5959
->write(!$tags ? "[],\n" : "['".implode("', '", array_keys($tags))."'],\n")
6060
->write(!$filters ? "[],\n" : "['".implode("', '", array_keys($filters))."'],\n")
61-
->write(!$functions ? "[]\n" : "['".implode("', '", array_keys($functions))."']\n")
61+
->write(!$functions ? "[],\n" : "['".implode("', '", array_keys($functions))."'],\n")
62+
->write("\$this->source\n")
6263
->outdent()
6364
->write(");\n")
6465
->outdent()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Twig.
5+
*
6+
* (c) Fabien Potencier
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Twig\Sandbox;
13+
14+
use Twig\Source;
15+
16+
/**
17+
* Interface for a class that can optionally enable the sandbox mode based on a template's Twig\Source.
18+
*
19+
* @author Yaakov Saxon
20+
*/
21+
interface SourcePolicyInterface
22+
{
23+
public function enableSandbox(Source $source): bool;
24+
}

tests/Extension/SandboxTest.php

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Twig\Sandbox\SecurityNotAllowedPropertyError;
2525
use Twig\Sandbox\SecurityNotAllowedTagError;
2626
use Twig\Sandbox\SecurityPolicy;
27+
use Twig\Source;
2728

2829
class SandboxTest extends TestCase
2930
{
@@ -440,7 +441,7 @@ public function testMultipleClassMatchesViaInheritanceInAllowedMethods()
440441
$twig_parent_first->load('1_childobj_childmethod')->render(self::$params);
441442
} catch (SecurityError $e) {
442443
$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');
443-
}
444+
}
444445

445446
try {
446447
$twig_child_first->load('1_childobj_parentmethod')->render(self::$params);
@@ -449,15 +450,50 @@ public function testMultipleClassMatchesViaInheritanceInAllowedMethods()
449450
}
450451
}
451452

452-
protected function getEnvironment($sandboxed, $options, $templates, $tags = [], $filters = [], $methods = [], $properties = [], $functions = [])
453+
protected function getEnvironment($sandboxed, $options, $templates, $tags = [], $filters = [], $methods = [], $properties = [], $functions = [], $sourcePolicy = null)
453454
{
454455
$loader = new ArrayLoader($templates);
455456
$twig = new Environment($loader, array_merge(['debug' => true, 'cache' => false, 'autoescape' => false], $options));
456457
$policy = new SecurityPolicy($tags, $filters, $methods, $properties, $functions);
457-
$twig->addExtension(new SandboxExtension($policy, $sandboxed));
458+
$twig->addExtension(new SandboxExtension($policy, $sandboxed, $sourcePolicy));
458459

459460
return $twig;
460461
}
462+
463+
public function testSandboxSourcePolicyEnableReturningFalse()
464+
{
465+
$twig = $this->getEnvironment(false, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface {
466+
public function enableSandbox(Source $source): bool
467+
{
468+
return '1_basic' != $source->getName();
469+
}
470+
});
471+
$this->assertEquals('FOO', $twig->load('1_basic')->render(self::$params));
472+
}
473+
474+
public function testSandboxSourcePolicyEnableReturningTrue()
475+
{
476+
$twig = $this->getEnvironment(false, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface {
477+
public function enableSandbox(Source $source): bool
478+
{
479+
return '1_basic' === $source->getName();
480+
}
481+
});
482+
$this->expectException(SecurityError::class);
483+
$twig->load('1_basic')->render([]);
484+
}
485+
486+
public function testSandboxSourcePolicyFalseDoesntOverrideOtherEnables()
487+
{
488+
$twig = $this->getEnvironment(true, [], self::$templates, [], [], [], [], [], new class() implements \Twig\Sandbox\SourcePolicyInterface {
489+
public function enableSandbox(Source $source): bool
490+
{
491+
return false;
492+
}
493+
});
494+
$this->expectException(SecurityError::class);
495+
$twig->load('1_basic')->render([]);
496+
}
461497
}
462498

463499
class ParentClass

0 commit comments

Comments
 (0)