<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core;
/**
* Unit tests for core renderer render template exploit.
*
* @package core
* @category test
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_renderer_template_exploit_test extends \advanced_testcase {
/**
* Test cases to confirm that blacklisted helpers are stripped from the source
* text by the helper before being passed to other another helper. This prevents
* nested calls to helpers.
*/
public function get_template_testcases() {
// Different helper implementations to test various combinations of nested
// calls to render the templates.
$norender = function($text) {
return $text;
};
$singlerender = function($text, $helper) {
return $helper->render($text);
};
$recursiverender = function($text, $helper) {
$result = $helper->render($text);
while (strpos($result, '{{') != false) {
$result = $helper->render($result);
}
return $result;
};
return [
'nested JS helper' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{#js}} some nasty JS {{/js}}{{/testpix}}',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
],
'other nested helper' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{#test1}} some text {{/test1}}{{/testpix}}',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $singlerender,
'test1' => $norender,
],
'js' => 'some nasty JS',
'expected' => 'core, move, some text',
'include' => false
],
'double nested helper' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{#test1}} some text {{#js}} some nasty JS {{/js}} {{/test1}}{{/testpix}}',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $singlerender,
'test1' => $norender,
],
'js' => 'some nasty JS',
'expected' => 'core, move, some text {{}}',
'include' => false
],
'js helper not nested' => [
'templates' => [
'test' => '{{#testpix}} core, move, some text {{/testpix}}{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move, some text',
'include' => true
],
'js in context not in helper' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{/testpix}}{{hack}}',
],
'torender' => 'test',
'context' => [
'hack' => '{{#js}} some nasty JS {{/js}}'
],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move, {{#js}} some nasty JS {{/js}}',
'include' => false
],
'js in context' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{hack}}{{/testpix}}',
],
'torender' => 'test',
'context' => [
'hack' => '{{#js}} some nasty JS {{/js}}'
],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move, {{}}',
'include' => false
],
'js in context double depth with single render' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{first}}{{/testpix}}',
],
'torender' => 'test',
'context' => [
'first' => '{{second}}',
'second' => '{{#js}} some nasty JS {{/js}}'
],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move, {{second}}',
'include' => false
],
'js in context double depth with recursive render' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{first}}{{/testpix}}',
],
'torender' => 'test',
'context' => [
'first' => '{{second}}',
'second' => '{{#js}} some nasty JS {{/js}}'
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
],
'partial' => [
'templates' => [
'test' => '{{#testpix}} core, move, blah{{/testpix}}, {{> test2}}',
'test2' => 'some content',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, blah, some content',
'include' => false
],
'partial nested' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{> test2}}{{/testpix}}',
'test2' => 'some content',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, some content',
'include' => false
],
'partial with js' => [
'templates' => [
'test' => '{{#testpix}} core, move, blah{{/testpix}}, {{> test2}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, blah,',
'include' => true
],
'partial nested with js' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{> test2}}{{/testpix}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
],
'partial with js from context' => [
'templates' => [
'test' => '{{#testpix}} core, move, blah{{/testpix}}, {{{foo}}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [
'foo' => '{{> test2}}'
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, blah, {{> test2}}',
'include' => false
],
'partial nested with js from context recursive render' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [
'foo' => '{{> test2}}'
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
],
'partial nested with js from context single render' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [
'foo' => '{{> test2}}'
],
'helpers' => [
'testpix' => $singlerender
],
'js' => 'some nasty JS',
'expected' => 'core, move, {{> test2}}',
'include' => false
],
'partial double nested with js from context recursive render' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{foo}}{{/testpix}}',
'test2' => '{{#js}} some nasty JS {{/js}}',
],
'torender' => 'test',
'context' => [
'foo' => '{{bar}}',
'bar' => '{{> test2}}'
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
],
'array context depth 1' => [
'templates' => [
'test' => '{{#items}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/items}}'
],
'torender' => 'test',
'context' => [
'items' => [
'legit',
'{{#js}}some nasty JS{{/js}}'
]
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, legit core, move,',
'include' => false
],
'array context depth 2' => [
'templates' => [
'test' => '{{#items}}{{#subitems}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/subitems}}{{/items}}'
],
'torender' => 'test',
'context' => [
'items' => [
[
'subitems' => [
'legit',
'{{#js}}some nasty JS{{/js}}'
]
],
]
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, legit core, move,',
'include' => false
],
'object context depth 1' => [
'templates' => [
'test' => '{{#items}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/items}}'
],
'torender' => 'test',
'context' => (object) [
'items' => [
'legit',
'{{#js}}some nasty JS{{/js}}'
]
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, legit core, move,',
'include' => false
],
'object context depth 2' => [
'templates' => [
'test' => '{{#items}}{{#subitems}}{{#testpix}} core, move, {{.}}{{/testpix}}{{/subitems}}{{/items}}'
],
'torender' => 'test',
'context' => (object) [
'items' => [
(object) [
'subitems' => [
'legit',
'{{#js}}some nasty JS{{/js}}'
]
],
]
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move, legit core, move,',
'include' => false
],
'change delimeters' => [
'templates' => [
'test' => '{{#testpix}} core, move, {{{foo}}}{{/testpix}}'
],
'torender' => 'test',
'context' => [
'foo' => '{{=<% %>=}} <%#js%>some nasty JS,<%/js%>'
],
'helpers' => [
'testpix' => $recursiverender
],
'js' => 'some nasty JS',
'expected' => 'core, move,',
'include' => false
]
];
}
/**
* Test that the mustache_helper_collection class correctly strips
* @dataProvider get_template_testcases()
< * @param string $templates The template to add
> * @param array $templates The template to add
* @param string $torender The name of the template to render
* @param array $context The template context
* @param array $helpers Mustache helpers to add
* @param string $js The JS string from the template
* @param string $expected The expected output of the string after stripping JS
* @param bool $include If the JS should be added to the page or not
*/
public function test_core_mustache_engine_strips_js_helper(
$templates,
$torender,
$context,
$helpers,
$js,
$expected,
$include
) {
$page = new \moodle_page();
$renderer = $page->get_renderer('core');
// Get the mustache engine from the renderer.
$reflection = new \ReflectionMethod($renderer, 'get_mustache');
$reflection->setAccessible(true);
$engine = $reflection->invoke($renderer);
// Swap the loader out with an array loader so that we can set some
// inline templates for testing.
$loader = new \Mustache_Loader_ArrayLoader([]);
$engine->setLoader($loader);
// Add our test helpers.
$helpercollection = $engine->getHelpers();
foreach ($helpers as $name => $function) {
$helpercollection->add($name, $function);
}
// Add our test template to be rendered.
foreach ($templates as $name => $template) {
$loader->setTemplate($name, $template);
}
// Confirm that the rendered template matches what we expect.
$this->assertEquals($expected, trim($engine->render($torender, $context)));
if ($include) {
// Confirm that the JS was added to the page.
$this->assertStringContainsString($js, $page->requires->get_end_code());
} else {
// Confirm that the JS wasn't added to the page.
$this->assertStringNotContainsString($js, $page->requires->get_end_code());
}
}
}