See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace core; 18 19 /** 20 * Unit tests for core renderer render template exploit. 21 * 22 * @package core 23 * @category test 24 * @copyright 2019 Ryan Wyllie <ryan@moodle.com> 25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 */ 27 class core_renderer_template_exploit_test extends \advanced_testcase { 28 /** 29 * Test cases to confirm that blacklisted helpers are stripped from the source 30 * text by the helper before being passed to other another helper. This prevents 31 * nested calls to helpers. 32 */ 33 public function get_template_testcases() { 34 // Different helper implementations to test various combinations of nested 35 // calls to render the templates. 36 $norender = function($text) { 37 return $text; 38 }; 39 $singlerender = function($text, $helper) { 40 return $helper->render($text); 41 }; 42 $recursiverender = function($text, $helper) { 43 $result = $helper->render($text); 44 45 while (strpos($result, '{{') != false) { 46 $result = $helper->render($result); 47 } 48 49 return $result; 50 }; 51 52 return [ 53 'nested JS helper' => [ 54 'templates' => [ 55 'test' => '{{#testpix}} core, move, {{#js}} some nasty JS {{/js}}{{/testpix}}', 56 ], 57 'torender' => 'test', 58 'context' => [], 59 'helpers' => [ 60 'testpix' => $singlerender 61 ], 62 'js' => 'some nasty JS', 63 'expected' => 'core, move,', 64 'include' => false 65 ], 66 'other nested helper' => [ 67 'templates' => [ 68 'test' => '{{#testpix}} core, move, {{#test1}} some text {{/test1}}{{/testpix}}', 69 ], 70 'torender' => 'test', 71 'context' => [], 72 'helpers' => [ 73 'testpix' => $singlerender, 74 'test1' => $norender, 75 ], 76 'js' => 'some nasty JS', 77 'expected' => 'core, move, some text', 78 'include' => false 79 ], 80 'double nested helper' => [ 81 'templates' => [ 82 'test' => '{{#testpix}} core, move, {{#test1}} some text {{#js}} some nasty JS {{/js}} {{/test1}}{{/testpix}}', 83 ], 84 'torender' => 'test', 85 'context' => [], 86 'helpers' => [ 87 'testpix' => $singlerender, 88 'test1' => $norender, 89 ], 90 'js' => 'some nasty JS', 91 'expected' => 'core, move, some text {{}}', 92 'include' => false 93 ], 94 'js helper not nested' => [ 95 'templates' => [ 96 'test' => '{{#testpix}} core, move, some text {{/testpix}}{{#js}} some nasty JS {{/js}}', 97 ], 98 'torender' => 'test', 99 'context' => [], 100 'helpers' => [ 101 'testpix' => $singlerender 102 ], 103 'js' => 'some nasty JS', 104 'expected' => 'core, move, some text', 105 'include' => true 106 ], 107 'js in context not in helper' => [ 108 'templates' => [ 109 'test' => '{{#testpix}} core, move, {{/testpix}}{{hack}}', 110 ], 111 'torender' => 'test', 112 'context' => [ 113 'hack' => '{{#js}} some nasty JS {{/js}}' 114 ], 115 'helpers' => [ 116 'testpix' => $singlerender 117 ], 118 'js' => 'some nasty JS', 119 'expected' => 'core, move, {{#js}} some nasty JS {{/js}}', 120 'include' => false 121 ], 122 'js in context' => [ 123 'templates' => [ 124 'test' => '{{#testpix}} core, move, {{hack}}{{/testpix}}', 125 ], 126 'torender' => 'test', 127 'context' => [ 128 'hack' => '{{#js}} some nasty JS {{/js}}' 129 ], 130 'helpers' => [ 131 'testpix' => $singlerender 132 ], 133 'js' => 'some nasty JS', 134 'expected' => 'core, move, {{}}', 135 'include' => false 136 ], 137 'js in context double depth with single render' => [ 138 'templates' => [ 139 'test' => '{{#testpix}} core, move, {{first}}{{/testpix}}', 140 ], 141 'torender' => 'test', 142 'context' => [ 143 'first' => '{{second}}', 144 'second' => '{{#js}} some nasty JS {{/js}}' 145 ], 146 'helpers' => [ 147 'testpix' => $singlerender 148 ], 149 'js' => 'some nasty JS', 150 'expected' => 'core, move, {{second}}', 151 'include' => false 152 ], 153 'js in context double depth with recursive render' => [ 154 'templates' => [ 155 'test' => '{{#testpix}} core, move, {{first}}{{/testpix}}', 156 ], 157 'torender' => 'test', 158 'context' => [ 159 'first' => '{{second}}', 160 'second' => '{{#js}} some nasty JS {{/js}}' 161 ], 162 'helpers' => [ 163 'testpix' => $recursiverender 164 ], 165 'js' => 'some nasty JS', 166 'expected' => 'core, move,', 167 'include' => false 168 ], 169 'partial' => [ 170 'templates' => [ 171 'test' => '{{#testpix}} core, move, blah{{/testpix}}, {{> test2}}', 172 'test2' => 'some content', 173 ], 174 'torender' => 'test', 175 'context' => [], 176 'helpers' => [ 177 'testpix' => $recursiverender 178 ], 179 'js' => 'some nasty JS', 180 'expected' => 'core, move, blah, some content', 181 'include' => false 182 ], 183 'partial nested' => [ 184 'templates' => [ 185 'test' => '{{#testpix}} core, move, {{> test2}}{{/testpix}}', 186 'test2' => 'some content', 187 ], 188 'torender' => 'test', 189 'context' => [], 190 'helpers' => [ 191 'testpix' => $recursiverender 192 ], 193 'js' => 'some nasty JS', 194 'expected' => 'core, move, some content', 195 'include' => false 196 ], 197 'partial with js' => [ 198 'templates' => [ 199 'test' => '{{#testpix}} core, move, blah{{/testpix}}, {{> test2}}', 200 'test2' => '{{#js}} some nasty JS {{/js}}', 201 ], 202 'torender' => 'test', 203 'context' => [], 204 'helpers' => [ 205 'testpix' => $recursiverender 206 ], 207 'js' => 'some nasty JS', 208 'expected' => 'core, move, blah,', 209 'include' => true 210 ], 211 'partial nested with js' => [ 212 'templates' => [ 213 'test' => '{{#testpix}} core, move, {{> test2}}{{/testpix}}', 214 'test2' => '{{#js}} some nasty JS {{/js}}', 215 ], 216 'torender' => 'test', 217 'context' => [], 218 'helpers' => [ 219 'testpix' => $recursiverender 220 ], 221 'js' => 'some nasty JS', 222 'expected' => 'core, move,', 223 'include' => false 224 ], 225 'partial with js from context' => [ 226 'templates' => [ 227 'test' => '{{#testpix}} core, move, blah{{/testpix}}, {{{foo}}}', 228 'test2' => '{{#js}} some nasty JS {{/js}}', 229 ], 230 'torender' => 'test', 231 'context' => [ 232 'foo' => '{{> test2}}' 233 ], 234 'helpers' => [ 235 'testpix' => $recursiverender 236 ], 237 'js' => 'some nasty JS', 238 'expected' => 'core, move, blah, {{> test2}}', 239 'include' => false 240 ], 241 'partial nested with js from context recursive render' => [ 242 'templates' => [ 243 'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}', 244 'test2' => '{{#js}} some nasty JS {{/js}}', 245 ], 246 'torender' => 'test', 247 'context' => [ 248 'foo' => '{{> test2}}' 249 ], 250 'helpers' => [ 251 'testpix' => $recursiverender 252 ], 253 'js' => 'some nasty JS', 254 'expected' => 'core, move,', 255 'include' => false 256 ], 257 'partial nested with js from context single render' => [ 258 'templates' => [ 259 'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}', 260 'test2' => '{{#js}} some nasty JS {{/js}}', 261 ], 262 'torender' => 'test', 263 'context' => [ 264 'foo' => '{{> test2}}' 265 ], 266 'helpers' => [ 267 'testpix' => $singlerender 268 ], 269 'js' => 'some nasty JS', 270 'expected' => 'core, move, {{> test2}}', 271 'include' => false 272 ], 273 'partial double nested with js from context recursive render' => [ 274 'templates' => [ 275 'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}', 276 'test2' => '{{#js}} some nasty JS {{/js}}', 277 ], 278 'torender' => 'test', 279 'context' => [ 280 'foo' => '{{bar}}', 281 'bar' => '{{> test2}}' 282 ], 283 'helpers' => [ 284 'testpix' => $recursiverender 285 ], 286 'js' => 'some nasty JS', 287 'expected' => 'core, move,', 288 'include' => false 289 ], 290 'array context depth 1' => [ 291 'templates' => [ 292 'test' => '{{#items}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/items}}' 293 ], 294 'torender' => 'test', 295 'context' => [ 296 'items' => [ 297 'legit', 298 '{{#js}}some nasty JS{{/js}}' 299 ] 300 ], 301 'helpers' => [ 302 'testpix' => $recursiverender 303 ], 304 'js' => 'some nasty JS', 305 'expected' => 'core, move, legit core, move,', 306 'include' => false 307 ], 308 'array context depth 2' => [ 309 'templates' => [ 310 'test' => '{{#items}}{{#subitems}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/subitems}}{{/items}}' 311 ], 312 'torender' => 'test', 313 'context' => [ 314 'items' => [ 315 [ 316 'subitems' => [ 317 'legit', 318 '{{#js}}some nasty JS{{/js}}' 319 ] 320 ], 321 ] 322 ], 323 'helpers' => [ 324 'testpix' => $recursiverender 325 ], 326 'js' => 'some nasty JS', 327 'expected' => 'core, move, legit core, move,', 328 'include' => false 329 ], 330 'object context depth 1' => [ 331 'templates' => [ 332 'test' => '{{#items}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/items}}' 333 ], 334 'torender' => 'test', 335 'context' => (object) [ 336 'items' => [ 337 'legit', 338 '{{#js}}some nasty JS{{/js}}' 339 ] 340 ], 341 'helpers' => [ 342 'testpix' => $recursiverender 343 ], 344 'js' => 'some nasty JS', 345 'expected' => 'core, move, legit core, move,', 346 'include' => false 347 ], 348 'object context depth 2' => [ 349 'templates' => [ 350 'test' => '{{#items}}{{#subitems}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/subitems}}{{/items}}' 351 ], 352 'torender' => 'test', 353 'context' => (object) [ 354 'items' => [ 355 (object) [ 356 'subitems' => [ 357 'legit', 358 '{{#js}}some nasty JS{{/js}}' 359 ] 360 ], 361 ] 362 ], 363 'helpers' => [ 364 'testpix' => $recursiverender 365 ], 366 'js' => 'some nasty JS', 367 'expected' => 'core, move, legit core, move,', 368 'include' => false 369 ], 370 'change delimeters' => [ 371 'templates' => [ 372 'test' => '{{#testpix}} core, move, {{{foo}}}{{/testpix}}' 373 ], 374 'torender' => 'test', 375 'context' => [ 376 'foo' => '{{=<% %>=}} <%#js%>some nasty JS,<%/js%>' 377 ], 378 'helpers' => [ 379 'testpix' => $recursiverender 380 ], 381 'js' => 'some nasty JS', 382 'expected' => 'core, move,', 383 'include' => false 384 ] 385 ]; 386 } 387 388 /** 389 * Test that the mustache_helper_collection class correctly strips 390 * @dataProvider get_template_testcases() 391 * @param string $templates The template to add 392 * @param string $torender The name of the template to render 393 * @param array $context The template context 394 * @param array $helpers Mustache helpers to add 395 * @param string $js The JS string from the template 396 * @param string $expected The expected output of the string after stripping JS 397 * @param bool $include If the JS should be added to the page or not 398 */ 399 public function test_core_mustache_engine_strips_js_helper( 400 $templates, 401 $torender, 402 $context, 403 $helpers, 404 $js, 405 $expected, 406 $include 407 ) { 408 $page = new \moodle_page(); 409 $renderer = $page->get_renderer('core'); 410 411 // Get the mustache engine from the renderer. 412 $reflection = new \ReflectionMethod($renderer, 'get_mustache'); 413 $reflection->setAccessible(true); 414 $engine = $reflection->invoke($renderer); 415 416 // Swap the loader out with an array loader so that we can set some 417 // inline templates for testing. 418 $loader = new \Mustache_Loader_ArrayLoader([]); 419 $engine->setLoader($loader); 420 421 // Add our test helpers. 422 $helpercollection = $engine->getHelpers(); 423 foreach ($helpers as $name => $function) { 424 $helpercollection->add($name, $function); 425 } 426 427 // Add our test template to be rendered. 428 foreach ($templates as $name => $template) { 429 $loader->setTemplate($name, $template); 430 } 431 432 // Confirm that the rendered template matches what we expect. 433 $this->assertEquals($expected, trim($engine->render($torender, $context))); 434 435 if ($include) { 436 // Confirm that the JS was added to the page. 437 $this->assertStringContainsString($js, $page->requires->get_end_code()); 438 } else { 439 // Confirm that the JS wasn't added to the page. 440 $this->assertStringNotContainsString($js, $page->requires->get_end_code()); 441 } 442 } 443 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body