Merge branch 'MDL-70125_39-3' of git://github.com/mdjnelson/moodle into MOODLE_39_STABLE
[moodle.git] / lib / tests / core_renderer_template_exploit_test.php
blob873ec025de8f0f9bbe62836cb7078de0654c4750
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.
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/>.
17 /**
18 * Unit tests for core renderer render template exploit.
20 * @copyright 2019 Ryan Wyllie <ryan@moodle.com>
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 defined('MOODLE_INTERNAL') || die();
26 /**
27 * Unit tests for core renderer render template exploit.
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.
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;
41 $singlerender = function($text, $helper) {
42 return $helper->render($text);
44 $recursiverender = function($text, $helper) {
45 $result = $helper->render($text);
47 while (strpos($result, '{{') != false) {
48 $result = $helper->render($result);
51 return $result;
54 return [
55 'nested JS helper' => [
56 'templates' => [
57 'test' => '{{#testpix}} core, move, {{#js}} some nasty JS {{/js}}{{/testpix}}',
59 'torender' => 'test',
60 'context' => [],
61 'helpers' => [
62 'testpix' => $singlerender
64 'js' => 'some nasty JS',
65 'expected' => 'core, move,',
66 'include' => false
68 'other nested helper' => [
69 'templates' => [
70 'test' => '{{#testpix}} core, move, {{#test1}} some text {{/test1}}{{/testpix}}',
72 'torender' => 'test',
73 'context' => [],
74 'helpers' => [
75 'testpix' => $singlerender,
76 'test1' => $norender,
78 'js' => 'some nasty JS',
79 'expected' => 'core, move, some text',
80 'include' => false
82 'double nested helper' => [
83 'templates' => [
84 'test' => '{{#testpix}} core, move, {{#test1}} some text {{#js}} some nasty JS {{/js}} {{/test1}}{{/testpix}}',
86 'torender' => 'test',
87 'context' => [],
88 'helpers' => [
89 'testpix' => $singlerender,
90 'test1' => $norender,
92 'js' => 'some nasty JS',
93 'expected' => 'core, move, some text',
94 'include' => false
96 'js helper not nested' => [
97 'templates' => [
98 'test' => '{{#testpix}} core, move, some text {{/testpix}}{{#js}} some nasty JS {{/js}}',
100 'torender' => 'test',
101 'context' => [],
102 'helpers' => [
103 'testpix' => $singlerender
105 'js' => 'some nasty JS',
106 'expected' => 'core, move, some text',
107 'include' => true
109 'js in context not in helper' => [
110 'templates' => [
111 'test' => '{{#testpix}} core, move, {{/testpix}}{{hack}}',
113 'torender' => 'test',
114 'context' => [
115 'hack' => '{{#js}} some nasty JS {{/js}}'
117 'helpers' => [
118 'testpix' => $singlerender
120 'js' => 'some nasty JS',
121 'expected' => 'core, move, {{#js}} some nasty JS {{/js}}',
122 'include' => false
124 'js in context' => [
125 'templates' => [
126 'test' => '{{#testpix}} core, move, {{hack}}{{/testpix}}',
128 'torender' => 'test',
129 'context' => [
130 'hack' => '{{#js}} some nasty JS {{/js}}'
132 'helpers' => [
133 'testpix' => $singlerender
135 'js' => 'some nasty JS',
136 'expected' => 'core, move,',
137 'include' => false
139 'js in context double depth with single render' => [
140 'templates' => [
141 'test' => '{{#testpix}} core, move, {{first}}{{/testpix}}',
143 'torender' => 'test',
144 'context' => [
145 'first' => '{{second}}',
146 'second' => '{{#js}} some nasty JS {{/js}}'
148 'helpers' => [
149 'testpix' => $singlerender
151 'js' => 'some nasty JS',
152 'expected' => 'core, move, {{#js}} some nasty JS {{/js}}',
153 'include' => false
155 'js in context double depth with recursive render' => [
156 'templates' => [
157 'test' => '{{#testpix}} core, move, {{first}}{{/testpix}}',
159 'torender' => 'test',
160 'context' => [
161 'first' => '{{second}}',
162 'second' => '{{#js}} some nasty JS {{/js}}'
164 'helpers' => [
165 'testpix' => $recursiverender
167 'js' => 'some nasty JS',
168 'expected' => 'core, move,',
169 'include' => false
171 'partial' => [
172 'templates' => [
173 'test' => '{{#testpix}} core, move, blah{{/testpix}}, {{> test2}}',
174 'test2' => 'some content',
176 'torender' => 'test',
177 'context' => [],
178 'helpers' => [
179 'testpix' => $recursiverender
181 'js' => 'some nasty JS',
182 'expected' => 'core, move, blah, some content',
183 'include' => false
185 'partial nested' => [
186 'templates' => [
187 'test' => '{{#testpix}} core, move, {{> test2}}{{/testpix}}',
188 'test2' => 'some content',
190 'torender' => 'test',
191 'context' => [],
192 'helpers' => [
193 'testpix' => $recursiverender
195 'js' => 'some nasty JS',
196 'expected' => 'core, move, some content',
197 'include' => false
199 'partial with js' => [
200 'templates' => [
201 'test' => '{{#testpix}} core, move, blah{{/testpix}}, {{> test2}}',
202 'test2' => '{{#js}} some nasty JS {{/js}}',
204 'torender' => 'test',
205 'context' => [],
206 'helpers' => [
207 'testpix' => $recursiverender
209 'js' => 'some nasty JS',
210 'expected' => 'core, move, blah,',
211 'include' => true
213 'partial nested with js' => [
214 'templates' => [
215 'test' => '{{#testpix}} core, move, {{> test2}}{{/testpix}}',
216 'test2' => '{{#js}} some nasty JS {{/js}}',
218 'torender' => 'test',
219 'context' => [],
220 'helpers' => [
221 'testpix' => $recursiverender
223 'js' => 'some nasty JS',
224 'expected' => 'core, move,',
225 'include' => false
227 'partial with js from context' => [
228 'templates' => [
229 'test' => '{{#testpix}} core, move, blah{{/testpix}}, {{{foo}}}',
230 'test2' => '{{#js}} some nasty JS {{/js}}',
232 'torender' => 'test',
233 'context' => [
234 'foo' => '{{> test2}}'
236 'helpers' => [
237 'testpix' => $recursiverender
239 'js' => 'some nasty JS',
240 'expected' => 'core, move, blah, {{> test2}}',
241 'include' => false
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}}',
248 'torender' => 'test',
249 'context' => [
250 'foo' => '{{> test2}}'
252 'helpers' => [
253 'testpix' => $recursiverender
255 'js' => 'some nasty JS',
256 'expected' => 'core, move,',
257 'include' => false
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}}',
264 'torender' => 'test',
265 'context' => [
266 'foo' => '{{> test2}}'
268 'helpers' => [
269 'testpix' => $singlerender
271 'js' => 'some nasty JS',
272 'expected' => 'core, move,',
273 'include' => false
275 'partial double nested with js from context single render' => [
276 'templates' => [
277 'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}',
278 'test2' => '{{#js}} some nasty JS {{/js}}',
280 'torender' => 'test',
281 'context' => [
282 'foo' => '{{{bar}}}',
283 'bar' => '{{> test2}}'
285 'helpers' => [
286 'testpix' => $singlerender
288 'js' => 'some nasty JS',
289 'expected' => 'core, move, {{> test2}}',
290 'include' => false
292 'partial double nested with js from context recursive render' => [
293 'templates' => [
294 'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}',
295 'test2' => '{{#js}} some nasty JS {{/js}}',
297 'torender' => 'test',
298 'context' => [
299 'foo' => '{{bar}}',
300 'bar' => '{{> test2}}'
302 'helpers' => [
303 'testpix' => $recursiverender
305 'js' => 'some nasty JS',
306 'expected' => 'core, move,',
307 'include' => false
309 'array context depth 1' => [
310 'templates' => [
311 'test' => '{{#items}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/items}}'
313 'torender' => 'test',
314 'context' => [
315 'items' => [
316 'legit',
317 '{{#js}}some nasty JS{{/js}}'
320 'helpers' => [
321 'testpix' => $recursiverender
323 'js' => 'some nasty JS',
324 'expected' => 'core, move, legit core, move,',
325 'include' => false
327 'array context depth 2' => [
328 'templates' => [
329 'test' => '{{#items}}{{#subitems}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/subitems}}{{/items}}'
331 'torender' => 'test',
332 'context' => [
333 'items' => [
335 'subitems' => [
336 'legit',
337 '{{#js}}some nasty JS{{/js}}'
342 'helpers' => [
343 'testpix' => $recursiverender
345 'js' => 'some nasty JS',
346 'expected' => 'core, move, legit core, move,',
347 'include' => false
349 'object context depth 1' => [
350 'templates' => [
351 'test' => '{{#items}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/items}}'
353 'torender' => 'test',
354 'context' => (object) [
355 'items' => [
356 'legit',
357 '{{#js}}some nasty JS{{/js}}'
360 'helpers' => [
361 'testpix' => $recursiverender
363 'js' => 'some nasty JS',
364 'expected' => 'core, move, legit core, move,',
365 'include' => false
367 'object context depth 2' => [
368 'templates' => [
369 'test' => '{{#items}}{{#subitems}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/subitems}}{{/items}}'
371 'torender' => 'test',
372 'context' => (object) [
373 'items' => [
374 (object) [
375 'subitems' => [
376 'legit',
377 '{{#js}}some nasty JS{{/js}}'
382 'helpers' => [
383 'testpix' => $recursiverender
385 'js' => 'some nasty JS',
386 'expected' => 'core, move, legit core, move,',
387 'include' => false
389 'change delimeters' => [
390 'templates' => [
391 'test' => '{{#testpix}} core, move, {{{foo}}}{{/testpix}}'
393 'torender' => 'test',
394 'context' => [
395 'foo' => '{{=<% %>=}} <%#js%>some nasty JS,<%/js%>'
397 'helpers' => [
398 'testpix' => $recursiverender
400 'js' => 'some nasty JS',
401 'expected' => 'core, move,',
402 'include' => false
408 * Test that the mustache_helper_collection class correctly strips
409 * @dataProvider get_template_testcases()
410 * @param string $templates The template to add
411 * @param string $torender The name of the template to render
412 * @param array $context The template context
413 * @param array $helpers Mustache helpers to add
414 * @param string $js The JS string from the template
415 * @param string $expected The expected output of the string after stripping JS
416 * @param bool $include If the JS should be added to the page or not
418 public function test_core_mustache_engine_strips_js_helper(
419 $templates,
420 $torender,
421 $context,
422 $helpers,
423 $js,
424 $expected,
425 $include
427 $page = new \moodle_page();
428 $renderer = $page->get_renderer('core');
430 // Get the mustache engine from the renderer.
431 $reflection = new \ReflectionMethod($renderer, 'get_mustache');
432 $reflection->setAccessible(true);
433 $engine = $reflection->invoke($renderer);
435 // Swap the loader out with an array loader so that we can set some
436 // inline templates for testing.
437 $loader = new \Mustache_Loader_ArrayLoader([]);
438 $engine->setLoader($loader);
440 // Add our test helpers.
441 $helpercollection = $engine->getHelpers();
442 foreach ($helpers as $name => $function) {
443 $helpercollection->add($name, $function);
446 // Add our test template to be rendered.
447 foreach ($templates as $name => $template) {
448 $loader->setTemplate($name, $template);
451 // Confirm that the rendered template matches what we expect.
452 $this->assertEquals($expected, trim($engine->render($torender, $context)));
454 if ($include) {
455 // Confirm that the JS was added to the page.
456 $this->assertContains($js, $page->requires->get_end_code());
457 } else {
458 // Confirm that the JS wasn't added to the page.
459 $this->assertNotContains($js, $page->requires->get_end_code());