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 12use Twig\Environment; 13use Twig\Extension\SandboxExtension; 14use Twig\Loader\ArrayLoader; 15use Twig\Sandbox\SecurityError; 16use Twig\Sandbox\SecurityPolicy; 17 18class Twig_Tests_Extension_SandboxTest extends \PHPUnit\Framework\TestCase 19{ 20 protected static $params; 21 protected static $templates; 22 23 protected function setUp() 24 { 25 self::$params = [ 26 'name' => 'Fabien', 27 'obj' => new FooObject(), 28 'arr' => ['obj' => new FooObject()], 29 ]; 30 31 self::$templates = [ 32 '1_basic1' => '{{ obj.foo }}', 33 '1_basic2' => '{{ name|upper }}', 34 '1_basic3' => '{% if name %}foo{% endif %}', 35 '1_basic4' => '{{ obj.bar }}', 36 '1_basic5' => '{{ obj }}', 37 '1_basic7' => '{{ cycle(["foo","bar"], 1) }}', 38 '1_basic8' => '{{ obj.getfoobar }}{{ obj.getFooBar }}', 39 '1_basic9' => '{{ obj.foobar }}{{ obj.fooBar }}', 40 '1_basic' => '{% if obj.foo %}{{ obj.foo|upper }}{% endif %}', 41 '1_layout' => '{% block content %}{% endblock %}', 42 '1_child' => "{% extends \"1_layout\" %}\n{% block content %}\n{{ \"a\"|json_encode }}\n{% endblock %}", 43 '1_include' => '{{ include("1_basic1", sandboxed=true) }}', 44 '1_range_operator' => '{{ (1..2)[0] }}', 45 ]; 46 } 47 48 /** 49 * @expectedException \Twig\Sandbox\SecurityError 50 * @expectedExceptionMessage Filter "json_encode" is not allowed in "1_child" at line 3. 51 */ 52 public function testSandboxWithInheritance() 53 { 54 $twig = $this->getEnvironment(true, [], self::$templates, ['block']); 55 $twig->load('1_child')->render([]); 56 } 57 58 public function testSandboxGloballySet() 59 { 60 $twig = $this->getEnvironment(false, [], self::$templates); 61 $this->assertEquals('FOO', $twig->load('1_basic')->render(self::$params), 'Sandbox does nothing if it is disabled globally'); 62 } 63 64 public function testSandboxUnallowedMethodAccessor() 65 { 66 $twig = $this->getEnvironment(true, [], self::$templates); 67 try { 68 $twig->load('1_basic1')->render(self::$params); 69 $this->fail('Sandbox throws a SecurityError exception if an unallowed method is called'); 70 } catch (SecurityError $e) { 71 $this->assertInstanceOf('\Twig\Sandbox\SecurityNotAllowedMethodError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedMethodError'); 72 $this->assertEquals('FooObject', $e->getClassName(), 'Exception should be raised on the "FooObject" class'); 73 $this->assertEquals('foo', $e->getMethodName(), 'Exception should be raised on the "foo" method'); 74 } 75 } 76 77 public function testSandboxUnallowedFilter() 78 { 79 $twig = $this->getEnvironment(true, [], self::$templates); 80 try { 81 $twig->load('1_basic2')->render(self::$params); 82 $this->fail('Sandbox throws a SecurityError exception if an unallowed filter is called'); 83 } catch (SecurityError $e) { 84 $this->assertInstanceOf('\Twig\Sandbox\SecurityNotAllowedFilterError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedFilterError'); 85 $this->assertEquals('upper', $e->getFilterName(), 'Exception should be raised on the "upper" filter'); 86 } 87 } 88 89 public function testSandboxUnallowedTag() 90 { 91 $twig = $this->getEnvironment(true, [], self::$templates); 92 try { 93 $twig->load('1_basic3')->render(self::$params); 94 $this->fail('Sandbox throws a SecurityError exception if an unallowed tag is used in the template'); 95 } catch (SecurityError $e) { 96 $this->assertInstanceOf('\Twig\Sandbox\SecurityNotAllowedTagError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedTagError'); 97 $this->assertEquals('if', $e->getTagName(), 'Exception should be raised on the "if" tag'); 98 } 99 } 100 101 public function testSandboxUnallowedProperty() 102 { 103 $twig = $this->getEnvironment(true, [], self::$templates); 104 try { 105 $twig->load('1_basic4')->render(self::$params); 106 $this->fail('Sandbox throws a SecurityError exception if an unallowed property is called in the template'); 107 } catch (SecurityError $e) { 108 $this->assertInstanceOf('\Twig\Sandbox\SecurityNotAllowedPropertyError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedPropertyError'); 109 $this->assertEquals('FooObject', $e->getClassName(), 'Exception should be raised on the "FooObject" class'); 110 $this->assertEquals('bar', $e->getPropertyName(), 'Exception should be raised on the "bar" property'); 111 } 112 } 113 114 /** 115 * @dataProvider getSandboxUnallowedToStringTests 116 */ 117 public function testSandboxUnallowedToString($template) 118 { 119 $twig = $this->getEnvironment(true, [], ['index' => $template], [], ['upper'], ['FooObject' => 'getAnotherFooObject'], [], ['random']); 120 try { 121 $twig->load('index')->render(self::$params); 122 $this->fail('Sandbox throws a SecurityError exception if an unallowed method (__toString()) is called in the template'); 123 } catch (SecurityError $e) { 124 $this->assertInstanceOf('\Twig\Sandbox\SecurityNotAllowedMethodError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedMethodError'); 125 $this->assertEquals('FooObject', $e->getClassName(), 'Exception should be raised on the "FooObject" class'); 126 $this->assertEquals('__tostring', $e->getMethodName(), 'Exception should be raised on the "__toString" method'); 127 } 128 } 129 130 public function getSandboxUnallowedToStringTests() 131 { 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'); 185 } 186 187 public function testSandboxUnallowedFunction() 188 { 189 $twig = $this->getEnvironment(true, [], self::$templates); 190 try { 191 $twig->load('1_basic7')->render(self::$params); 192 $this->fail('Sandbox throws a SecurityError exception if an unallowed function is called in the template'); 193 } catch (SecurityError $e) { 194 $this->assertInstanceOf('\Twig\Sandbox\SecurityNotAllowedFunctionError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedFunctionError'); 195 $this->assertEquals('cycle', $e->getFunctionName(), 'Exception should be raised on the "cycle" function'); 196 } 197 } 198 199 public function testSandboxUnallowedRangeOperator() 200 { 201 $twig = $this->getEnvironment(true, [], self::$templates); 202 try { 203 $twig->load('1_range_operator')->render(self::$params); 204 $this->fail('Sandbox throws a SecurityError exception if the unallowed range operator is called'); 205 } catch (SecurityError $e) { 206 $this->assertInstanceOf('\Twig\Sandbox\SecurityNotAllowedFunctionError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedFunctionError'); 207 $this->assertEquals('range', $e->getFunctionName(), 'Exception should be raised on the "range" function'); 208 } 209 } 210 211 public function testSandboxAllowMethodFoo() 212 { 213 $twig = $this->getEnvironment(true, [], self::$templates, [], [], ['FooObject' => 'foo']); 214 FooObject::reset(); 215 $this->assertEquals('foo', $twig->load('1_basic1')->render(self::$params), 'Sandbox allow some methods'); 216 $this->assertEquals(1, FooObject::$called['foo'], 'Sandbox only calls method once'); 217 } 218 219 public function testSandboxAllowFilter() 220 { 221 $twig = $this->getEnvironment(true, [], self::$templates, [], ['upper']); 222 $this->assertEquals('FABIEN', $twig->load('1_basic2')->render(self::$params), 'Sandbox allow some filters'); 223 } 224 225 public function testSandboxAllowTag() 226 { 227 $twig = $this->getEnvironment(true, [], self::$templates, ['if']); 228 $this->assertEquals('foo', $twig->load('1_basic3')->render(self::$params), 'Sandbox allow some tags'); 229 } 230 231 public function testSandboxAllowProperty() 232 { 233 $twig = $this->getEnvironment(true, [], self::$templates, [], [], [], ['FooObject' => 'bar']); 234 $this->assertEquals('bar', $twig->load('1_basic4')->render(self::$params), 'Sandbox allow some properties'); 235 } 236 237 public function testSandboxAllowFunction() 238 { 239 $twig = $this->getEnvironment(true, [], self::$templates, [], [], [], [], ['cycle']); 240 $this->assertEquals('bar', $twig->load('1_basic7')->render(self::$params), 'Sandbox allow some functions'); 241 } 242 243 public function testSandboxAllowRangeOperator() 244 { 245 $twig = $this->getEnvironment(true, [], self::$templates, [], [], [], [], ['range']); 246 $this->assertEquals('1', $twig->load('1_range_operator')->render(self::$params), 'Sandbox allow the range operator'); 247 } 248 249 public function testSandboxAllowFunctionsCaseInsensitive() 250 { 251 foreach (['getfoobar', 'getFoobar', 'getFooBar'] as $name) { 252 $twig = $this->getEnvironment(true, [], self::$templates, [], [], ['FooObject' => $name]); 253 FooObject::reset(); 254 $this->assertEquals('foobarfoobar', $twig->load('1_basic8')->render(self::$params), 'Sandbox allow methods in a case-insensitive way'); 255 $this->assertEquals(2, FooObject::$called['getFooBar'], 'Sandbox only calls method once'); 256 257 $this->assertEquals('foobarfoobar', $twig->load('1_basic9')->render(self::$params), 'Sandbox allow methods via shortcut names (ie. without get/set)'); 258 } 259 } 260 261 public function testSandboxLocallySetForAnInclude() 262 { 263 self::$templates = [ 264 '2_basic' => '{{ obj.foo }}{% include "2_included" %}{{ obj.foo }}', 265 '2_included' => '{% if obj.foo %}{{ obj.foo|upper }}{% endif %}', 266 ]; 267 268 $twig = $this->getEnvironment(false, [], self::$templates); 269 $this->assertEquals('fooFOOfoo', $twig->load('2_basic')->render(self::$params), 'Sandbox does nothing if disabled globally and sandboxed not used for the include'); 270 271 self::$templates = [ 272 '3_basic' => '{{ obj.foo }}{% sandbox %}{% include "3_included" %}{% endsandbox %}{{ obj.foo }}', 273 '3_included' => '{% if obj.foo %}{{ obj.foo|upper }}{% endif %}', 274 ]; 275 276 $twig = $this->getEnvironment(true, [], self::$templates); 277 try { 278 $twig->load('3_basic')->render(self::$params); 279 $this->fail('Sandbox throws a SecurityError exception when the included file is sandboxed'); 280 } catch (SecurityError $e) { 281 $this->assertInstanceOf('\Twig\Sandbox\SecurityNotAllowedTagError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedTagError'); 282 $this->assertEquals('sandbox', $e->getTagName()); 283 } 284 } 285 286 public function testMacrosInASandbox() 287 { 288 $twig = $this->getEnvironment(true, ['autoescape' => 'html'], ['index' => <<<EOF 289{%- import _self as macros %} 290 291{%- macro test(text) %}<p>{{ text }}</p>{% endmacro %} 292 293{{- macros.test('username') }} 294EOF 295 ], ['macro', 'import'], ['escape']); 296 297 $this->assertEquals('<p>username</p>', $twig->load('index')->render([])); 298 } 299 300 public function testSandboxDisabledAfterIncludeFunctionError() 301 { 302 $twig = $this->getEnvironment(false, [], self::$templates); 303 304 $e = null; 305 try { 306 $twig->load('1_include')->render(self::$params); 307 } catch (\Throwable $e) { 308 } catch (\Exception $e) { 309 } 310 if (null === $e) { 311 $this->fail('An exception should be thrown for this test to be valid.'); 312 } 313 314 $this->assertFalse($twig->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed(), 'Sandboxed include() function call should not leave Sandbox enabled when an error occurs.'); 315 } 316 317 protected function getEnvironment($sandboxed, $options, $templates, $tags = [], $filters = [], $methods = [], $properties = [], $functions = []) 318 { 319 $loader = new ArrayLoader($templates); 320 $twig = new Environment($loader, array_merge(['debug' => true, 'cache' => false, 'autoescape' => false], $options)); 321 $policy = new SecurityPolicy($tags, $filters, $methods, $properties, $functions); 322 $twig->addExtension(new SandboxExtension($policy, $sandboxed)); 323 324 return $twig; 325 } 326} 327 328class FooObject 329{ 330 public static $called = ['__toString' => 0, 'foo' => 0, 'getFooBar' => 0]; 331 332 public $bar = 'bar'; 333 334 public static function reset() 335 { 336 self::$called = ['__toString' => 0, 'foo' => 0, 'getFooBar' => 0]; 337 } 338 339 public function __toString() 340 { 341 ++self::$called['__toString']; 342 343 return 'foo'; 344 } 345 346 public function foo() 347 { 348 ++self::$called['foo']; 349 350 return 'foo'; 351 } 352 353 public function getFooBar() 354 { 355 ++self::$called['getFooBar']; 356 357 return 'foobar'; 358 } 359 360 public function getAnotherFooObject() 361 { 362 return new self(); 363 } 364} 365