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