... | ... |
@@ -1,5 +1,8 @@ |
1 | 1 |
* 1.38.0 (2019-XX-XX) |
2 | 2 |
|
3 |
+ * fixed sandbox security issue (under some circumstances, calling the |
|
4 |
+ __toString() method on an object was possible even if not allowed by the |
|
5 |
+ security policy) |
|
3 | 6 |
* fixed batch filter clobbers array keys when fill parameter is used |
4 | 7 |
* added preserveKeys support for the batch filter |
5 | 8 |
* fixed "embed" support when used from "template_from_string" |
6 | 9 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,42 @@ |
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\Node; |
|
13 |
+ |
|
14 |
+use Twig\Compiler; |
|
15 |
+use Twig\Node\Expression\AbstractExpression; |
|
16 |
+ |
|
17 |
+/** |
|
18 |
+ * Checks if casting an expression to __toString() is allowed by the sandbox. |
|
19 |
+ * |
|
20 |
+ * For instance, when there is a simple Print statement, like {{ article }}, |
|
21 |
+ * and if the sandbox is enabled, we need to check that the __toString() |
|
22 |
+ * method is allowed if 'article' is an object. The same goes for {{ article|upper }} |
|
23 |
+ * or {{ random(article) }} |
|
24 |
+ * |
|
25 |
+ * @author Fabien Potencier <fabien@symfony.com> |
|
26 |
+ */ |
|
27 |
+class CheckToStringNode extends Node |
|
28 |
+{ |
|
29 |
+ public function __construct(AbstractExpression $expr) |
|
30 |
+ { |
|
31 |
+ parent::__construct(['expr' => $expr], [], $expr->getTemplateLine(), $expr->getNodeTag()); |
|
32 |
+ } |
|
33 |
+ |
|
34 |
+ public function compile(Compiler $compiler) |
|
35 |
+ { |
|
36 |
+ $compiler |
|
37 |
+ ->raw('$this->sandbox->ensureToStringAllowed(') |
|
38 |
+ ->subcompile($this->getNode('expr')) |
|
39 |
+ ->raw(')') |
|
40 |
+ ; |
|
41 |
+ } |
|
42 |
+} |
... | ... |
@@ -22,6 +22,8 @@ use Twig\Node\Expression\FilterExpression; |
22 | 22 |
* and if the sandbox is enabled, we need to check that the __toString() |
23 | 23 |
* method is allowed if 'article' is an object. |
24 | 24 |
* |
25 |
+ * Not used anymore, to be deprecated in 2.x and removed in 3.0 |
|
26 |
+ * |
|
25 | 27 |
* @author Fabien Potencier <fabien@symfony.com> |
26 | 28 |
*/ |
27 | 29 |
class SandboxedPrintNode extends PrintNode |
... | ... |
@@ -13,13 +13,17 @@ namespace Twig\NodeVisitor; |
13 | 13 |
|
14 | 14 |
use Twig\Environment; |
15 | 15 |
use Twig\Node\CheckSecurityNode; |
16 |
+use Twig\Node\CheckToStringNode; |
|
17 |
+use Twig\Node\Expression\Binary\ConcatBinary; |
|
16 | 18 |
use Twig\Node\Expression\Binary\RangeBinary; |
17 | 19 |
use Twig\Node\Expression\FilterExpression; |
18 | 20 |
use Twig\Node\Expression\FunctionExpression; |
21 |
+use Twig\Node\Expression\GetAttrExpression; |
|
22 |
+use Twig\Node\Expression\NameExpression; |
|
19 | 23 |
use Twig\Node\ModuleNode; |
20 | 24 |
use Twig\Node\Node; |
21 | 25 |
use Twig\Node\PrintNode; |
22 |
-use Twig\Node\SandboxedPrintNode; |
|
26 |
+use Twig\Node\SetNode; |
|
23 | 27 |
|
24 | 28 |
/** |
25 | 29 |
* @final |
... | ... |
@@ -33,6 +37,8 @@ class SandboxNodeVisitor extends AbstractNodeVisitor |
33 | 37 |
protected $filters; |
34 | 38 |
protected $functions; |
35 | 39 |
|
40 |
+ private $needsToStringWrap = false; |
|
41 |
+ |
|
36 | 42 |
protected function doEnterNode(Node $node, Environment $env) |
37 | 43 |
{ |
38 | 44 |
if ($node instanceof ModuleNode) { |
... | ... |
@@ -63,9 +69,28 @@ class SandboxNodeVisitor extends AbstractNodeVisitor |
63 | 69 |
$this->functions['range'] = $node; |
64 | 70 |
} |
65 | 71 |
|
66 |
- // wrap print to check __toString() calls |
|
67 | 72 |
if ($node instanceof PrintNode) { |
68 |
- return new SandboxedPrintNode($node->getNode('expr'), $node->getTemplateLine(), $node->getNodeTag()); |
|
73 |
+ $this->needsToStringWrap = true; |
|
74 |
+ $this->wrapNode($node, 'expr'); |
|
75 |
+ } |
|
76 |
+ |
|
77 |
+ if ($node instanceof SetNode && !$node->getAttribute('capture')) { |
|
78 |
+ $this->needsToStringWrap = true; |
|
79 |
+ } |
|
80 |
+ |
|
81 |
+ // wrap outer nodes that can implicitly call __toString() |
|
82 |
+ if ($this->needsToStringWrap) { |
|
83 |
+ if ($node instanceof ConcatBinary) { |
|
84 |
+ $this->wrapNode($node, 'left'); |
|
85 |
+ $this->wrapNode($node, 'right'); |
|
86 |
+ } |
|
87 |
+ if ($node instanceof FilterExpression) { |
|
88 |
+ $this->wrapNode($node, 'node'); |
|
89 |
+ $this->wrapArrayNode($node, 'arguments'); |
|
90 |
+ } |
|
91 |
+ if ($node instanceof FunctionExpression) { |
|
92 |
+ $this->wrapArrayNode($node, 'arguments'); |
|
93 |
+ } |
|
69 | 94 |
} |
70 | 95 |
} |
71 | 96 |
|
... | ... |
@@ -78,11 +103,31 @@ class SandboxNodeVisitor extends AbstractNodeVisitor |
78 | 103 |
$this->inAModule = false; |
79 | 104 |
|
80 | 105 |
$node->setNode('constructor_end', new Node([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('display_start')])); |
106 |
+ } elseif ($this->inAModule) { |
|
107 |
+ if ($node instanceof PrintNode || $node instanceof SetNode) { |
|
108 |
+ $this->needsToStringWrap = false; |
|
109 |
+ } |
|
81 | 110 |
} |
82 | 111 |
|
83 | 112 |
return $node; |
84 | 113 |
} |
85 | 114 |
|
115 |
+ private function wrapNode(Node $node, $name) |
|
116 |
+ { |
|
117 |
+ $expr = $node->getNode($name); |
|
118 |
+ if ($expr instanceof NameExpression || $expr instanceof GetAttrExpression) { |
|
119 |
+ $node->setNode($name, new CheckToStringNode($expr)); |
|
120 |
+ } |
|
121 |
+ } |
|
122 |
+ |
|
123 |
+ private function wrapArrayNode(Node $node, $name) |
|
124 |
+ { |
|
125 |
+ $args = $node->getNode($name); |
|
126 |
+ foreach ($args as $name => $_) { |
|
127 |
+ $this->wrapNode($args, $name); |
|
128 |
+ } |
|
129 |
+ } |
|
130 |
+ |
|
86 | 131 |
public function getPriority() |
87 | 132 |
{ |
88 | 133 |
return 0; |
... | ... |
@@ -34,7 +34,6 @@ class Twig_Tests_Extension_SandboxTest extends \PHPUnit\Framework\TestCase |
34 | 34 |
'1_basic3' => '{% if name %}foo{% endif %}', |
35 | 35 |
'1_basic4' => '{{ obj.bar }}', |
36 | 36 |
'1_basic5' => '{{ obj }}', |
37 |
- '1_basic6' => '{{ arr.obj }}', |
|
38 | 37 |
'1_basic7' => '{{ cycle(["foo","bar"], 1) }}', |
39 | 38 |
'1_basic8' => '{{ obj.getfoobar }}{{ obj.getFooBar }}', |
40 | 39 |
'1_basic9' => '{{ obj.foobar }}{{ obj.fooBar }}', |
... | ... |
@@ -112,11 +111,14 @@ class Twig_Tests_Extension_SandboxTest extends \PHPUnit\Framework\TestCase |
112 | 111 |
} |
113 | 112 |
} |
114 | 113 |
|
115 |
- public function testSandboxUnallowedToString() |
|
114 |
+ /** |
|
115 |
+ * @dataProvider getSandboxUnallowedToStringTests |
|
116 |
+ */ |
|
117 |
+ public function testSandboxUnallowedToString($template) |
|
116 | 118 |
{ |
117 |
- $twig = $this->getEnvironment(true, [], self::$templates); |
|
119 |
+ $twig = $this->getEnvironment(true, [], ['index' => $template], [], ['upper'], ['FooObject' => 'getAnotherFooObject'], [], ['random']); |
|
118 | 120 |
try { |
119 |
- $twig->load('1_basic5')->render(self::$params); |
|
121 |
+ $twig->loadTemplate('index')->render(self::$params); |
|
120 | 122 |
$this->fail('Sandbox throws a SecurityError exception if an unallowed method (__toString()) is called in the template'); |
121 | 123 |
} catch (SecurityError $e) { |
122 | 124 |
$this->assertInstanceOf('\Twig\Sandbox\SecurityNotAllowedMethodError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedMethodError'); |
... | ... |
@@ -125,17 +127,61 @@ class Twig_Tests_Extension_SandboxTest extends \PHPUnit\Framework\TestCase |
125 | 127 |
} |
126 | 128 |
} |
127 | 129 |
|
128 |
- public function testSandboxUnallowedToStringArray() |
|
130 |
+ public function getSandboxUnallowedToStringTests() |
|
129 | 131 |
{ |
130 |
- $twig = $this->getEnvironment(true, [], self::$templates); |
|
131 |
- try { |
|
132 |
- $twig->load('1_basic6')->render(self::$params); |
|
133 |
- $this->fail('Sandbox throws a SecurityError exception if an unallowed method (__toString()) is called in the template'); |
|
134 |
- } catch (SecurityError $e) { |
|
135 |
- $this->assertInstanceOf('\Twig\Sandbox\SecurityNotAllowedMethodError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedMethodError'); |
|
136 |
- $this->assertEquals('FooObject', $e->getClassName(), 'Exception should be raised on the "FooObject" class'); |
|
137 |
- $this->assertEquals('__tostring', $e->getMethodName(), 'Exception should be raised on the "__toString" method'); |
|
138 |
- } |
|
132 |
+ return [ |
|
133 |
+ 'simple' => ['{{ obj }}'], |
|
134 |
+ 'object_from_array' => ['{{ arr.obj }}'], |
|
135 |
+ 'object_chain' => ['{{ obj.anotherFooObject }}'], |
|
136 |
+ 'filter' => ['{{ obj|upper }}'], |
|
137 |
+ 'filter_from_array' => ['{{ arr.obj|upper }}'], |
|
138 |
+ 'function' => ['{{ random(obj) }}'], |
|
139 |
+ 'function_from_array' => ['{{ random(arr.obj) }}'], |
|
140 |
+ 'function_and_filter' => ['{{ random(obj|upper) }}'], |
|
141 |
+ 'function_and_filter_from_array' => ['{{ random(arr.obj|upper) }}'], |
|
142 |
+ 'object_chain_and_filter' => ['{{ obj.anotherFooObject|upper }}'], |
|
143 |
+ 'object_chain_and_function' => ['{{ random(obj.anotherFooObject) }}'], |
|
144 |
+ 'concat' => ['{{ obj ~ "" }}'], |
|
145 |
+ 'concat_again' => ['{{ "" ~ obj }}'], |
|
146 |
+ ]; |
|
147 |
+ } |
|
148 |
+ |
|
149 |
+ /** |
|
150 |
+ * @dataProvider getSandboxAllowedToStringTests |
|
151 |
+ */ |
|
152 |
+ public function testSandboxAllowedToString($template, $output) |
|
153 |
+ { |
|
154 |
+ $twig = $this->getEnvironment(true, [], ['index' => $template], ['set'], [], ['FooObject' => ['foo', 'getAnotherFooObject']]); |
|
155 |
+ $this->assertEquals($output, $twig->load('index')->render(self::$params)); |
|
156 |
+ } |
|
157 |
+ |
|
158 |
+ public function getSandboxAllowedToStringTests() |
|
159 |
+ { |
|
160 |
+ return [ |
|
161 |
+ 'constant_test' => ['{{ obj is constant("PHP_INT_MAX") }}', ''], |
|
162 |
+ 'set_object' => ['{% set a = obj.anotherFooObject %}{{ a.foo }}', 'foo'], |
|
163 |
+ 'is_defined' => ['{{ obj.anotherFooObject is defined }}', '1'], |
|
164 |
+ 'is_null' => ['{{ obj is null }}', ''], |
|
165 |
+ 'is_sameas' => ['{{ obj is same as(obj) }}', '1'], |
|
166 |
+ 'is_sameas_from_array' => ['{{ arr.obj is same as(arr.obj) }}', '1'], |
|
167 |
+ 'is_sameas_from_another_method' => ['{{ obj.anotherFooObject is same as(obj.anotherFooObject) }}', ''], |
|
168 |
+ ]; |
|
169 |
+ } |
|
170 |
+ |
|
171 |
+ public function testSandboxAllowMethodToString() |
|
172 |
+ { |
|
173 |
+ $twig = $this->getEnvironment(true, [], self::$templates, [], [], ['FooObject' => '__toString']); |
|
174 |
+ FooObject::reset(); |
|
175 |
+ $this->assertEquals('foo', $twig->load('1_basic5')->render(self::$params), 'Sandbox allow some methods'); |
|
176 |
+ $this->assertEquals(1, FooObject::$called['__toString'], 'Sandbox only calls method once'); |
|
177 |
+ } |
|
178 |
+ |
|
179 |
+ public function testSandboxAllowMethodToStringDisabled() |
|
180 |
+ { |
|
181 |
+ $twig = $this->getEnvironment(false, [], self::$templates); |
|
182 |
+ FooObject::reset(); |
|
183 |
+ $this->assertEquals('foo', $twig->load('1_basic5')->render(self::$params), 'Sandbox allows __toString when sandbox disabled'); |
|
184 |
+ $this->assertEquals(1, FooObject::$called['__toString'], 'Sandbox only calls method once'); |
|
139 | 185 |
} |
140 | 186 |
|
141 | 187 |
public function testSandboxUnallowedFunction() |
... | ... |
@@ -170,22 +216,6 @@ class Twig_Tests_Extension_SandboxTest extends \PHPUnit\Framework\TestCase |
170 | 216 |
$this->assertEquals(1, FooObject::$called['foo'], 'Sandbox only calls method once'); |
171 | 217 |
} |
172 | 218 |
|
173 |
- public function testSandboxAllowMethodToString() |
|
174 |
- { |
|
175 |
- $twig = $this->getEnvironment(true, [], self::$templates, [], [], ['FooObject' => '__toString']); |
|
176 |
- FooObject::reset(); |
|
177 |
- $this->assertEquals('foo', $twig->load('1_basic5')->render(self::$params), 'Sandbox allow some methods'); |
|
178 |
- $this->assertEquals(1, FooObject::$called['__toString'], 'Sandbox only calls method once'); |
|
179 |
- } |
|
180 |
- |
|
181 |
- public function testSandboxAllowMethodToStringDisabled() |
|
182 |
- { |
|
183 |
- $twig = $this->getEnvironment(false, [], self::$templates); |
|
184 |
- FooObject::reset(); |
|
185 |
- $this->assertEquals('foo', $twig->load('1_basic5')->render(self::$params), 'Sandbox allows __toString when sandbox disabled'); |
|
186 |
- $this->assertEquals(1, FooObject::$called['__toString'], 'Sandbox only calls method once'); |
|
187 |
- } |
|
188 |
- |
|
189 | 219 |
public function testSandboxAllowFilter() |
190 | 220 |
{ |
191 | 221 |
$twig = $this->getEnvironment(true, [], self::$templates, [], ['upper']); |
... | ... |
@@ -326,4 +356,9 @@ class FooObject |
326 | 356 |
|
327 | 357 |
return 'foobar'; |
328 | 358 |
} |
359 |
+ |
|
360 |
+ public function getAnotherFooObject() |
|
361 |
+ { |
|
362 |
+ return new self(); |
|
363 |
+ } |
|
329 | 364 |
} |
330 | 365 |
deleted file mode 100644 |
... | ... |
@@ -1,42 +0,0 @@ |
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 |
-use Twig\Node\Expression\ConstantExpression; |
|
13 |
-use Twig\Node\Expression\NameExpression; |
|
14 |
-use Twig\Node\SandboxedPrintNode; |
|
15 |
-use Twig\Test\NodeTestCase; |
|
16 |
- |
|
17 |
-class Twig_Tests_Node_SandboxedPrintTest extends NodeTestCase |
|
18 |
-{ |
|
19 |
- public function testConstructor() |
|
20 |
- { |
|
21 |
- $node = new SandboxedPrintNode($expr = new ConstantExpression('foo', 1), 1); |
|
22 |
- |
|
23 |
- $this->assertEquals($expr, $node->getNode('expr')); |
|
24 |
- } |
|
25 |
- |
|
26 |
- public function getTests() |
|
27 |
- { |
|
28 |
- $tests[] = [new SandboxedPrintNode(new ConstantExpression('foo', 1), 1), <<<EOF |
|
29 |
-// line 1 |
|
30 |
-echo "foo"; |
|
31 |
-EOF |
|
32 |
- ]; |
|
33 |
- |
|
34 |
- $tests[] = [new SandboxedPrintNode(new NameExpression('foo', 1), 1), <<<EOF |
|
35 |
-// line 1 |
|
36 |
-echo \$this->env->getExtension('\Twig\Extension\SandboxExtension')->ensureToStringAllowed({$this->getVariableGetter('foo', false)}); |
|
37 |
-EOF |
|
38 |
- ]; |
|
39 |
- |
|
40 |
- return $tests; |
|
41 |
- } |
|
42 |
-} |