diff --git a/src/Twig/FileLoader.php b/src/Twig/FileLoader.php index e159fe6..6bc1d60 100644 --- a/src/Twig/FileLoader.php +++ b/src/Twig/FileLoader.php @@ -93,8 +93,26 @@ public function exists(string $name) */ public function findTemplate(string $name): string { + $templatePaths = App::path('templates'); if (file_exists($name)) { - return $name; + $name = str_replace('//', '/', $name); + // Check both app template paths and all plugins, + // as template from element() end up here too. + // We also need to protect against `{% include var_name %}` + // where var_name is request data. + foreach ($templatePaths as $templatePath) { + if (str_starts_with($name, $templatePath)) { + return $name; + } + } + foreach (Plugin::loaded() as $pluginName) { + $pluginPath = Plugin::templatePath($pluginName); + if (str_starts_with($name, $pluginPath)) { + return $name; + } + } + + throw $this->loaderError($name, $templatePaths); } [$plugin, $name] = pluginSplit($name); @@ -106,24 +124,35 @@ public function findTemplate(string $name): string if ($path !== null) { return $path; } - $error = "Could not find template `{$name}` in plugin `{$plugin}` in these paths:\n\n" . "- `{$templatePath}`\n"; throw new LoaderError($error); } - foreach (App::path('templates') as $templatePath) { + foreach ($templatePaths as $templatePath) { $path = $this->checkExtensions($templatePath . $name); if ($path !== null) { return $path; } } + throw $this->loaderError($name, $templatePaths); + } + /** + * Create a LoaderError with template path list in the message. + * + * @param string $name The name of the template that could not be found. + * @param array $templatePaths List of template paths that were searched + * @return \Twig\Error\LoaderError + */ + protected function loaderError(string $name, array $templatePaths): LoaderError + { $error = "Could not find template `{$name}` in these paths:\n\n"; - foreach (App::path('templates') as $templatePath) { + foreach ($templatePaths as $templatePath) { $error .= "- `{$templatePath}`\n"; } - throw new LoaderError($error); + + return new LoaderError($error); } /** diff --git a/tests/TestCase/Twig/FileLoaderTest.php b/tests/TestCase/Twig/FileLoaderTest.php index 05752e8..0577a72 100644 --- a/tests/TestCase/Twig/FileLoaderTest.php +++ b/tests/TestCase/Twig/FileLoaderTest.php @@ -83,13 +83,14 @@ public function testGetCacheKeyPluginNonExistingFile(): void public function testIsFresh(): void { - file_put_contents(TMP . 'TwigViewIsFreshTest', 'is fresh test'); - $time = filemtime(TMP . 'TwigViewIsFreshTest'); + $path = TEST_APP . 'templates/test_is_fresh.twig'; + file_put_contents($path, 'is fresh test'); + $time = filemtime($path); - $this->assertTrue($this->loader->isFresh(TMP . 'TwigViewIsFreshTest', $time + 5)); - $this->assertTrue(!$this->loader->isFresh(TMP . 'TwigViewIsFreshTest', $time - 5)); + $this->assertTrue($this->loader->isFresh($path, $time + 5)); + $this->assertTrue(!$this->loader->isFresh($path, $time - 5)); - unlink(TMP . 'TwigViewIsFreshTest'); + unlink($path); } public function testIsFreshNonExistingFile(): void diff --git a/tests/TestCase/View/TwigViewTest.php b/tests/TestCase/View/TwigViewTest.php index 06a0bc1..294e90c 100644 --- a/tests/TestCase/View/TwigViewTest.php +++ b/tests/TestCase/View/TwigViewTest.php @@ -24,6 +24,7 @@ use Cake\I18n\I18n; use Cake\TestSuite\TestCase; use TestApp\View\AppView; +use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\Extra\Markdown\DefaultMarkdown; @@ -256,6 +257,17 @@ public function testThrowSyntaxError(): void $this->view->render('syntaxerror', false); } + public function testTemplatePathRestriction(): void + { + $path = TMP . 'secret.txt'; + file_put_contents($path, 'SECRET DATA from /tmp/secret.txt'); + $view = new AppView(); + $view->set('item', $path); + $this->expectException(LoaderError::class); + $this->expectExceptionMessage('Could not find template'); + $view->render('template_path_restriction'); + } + public function testHelperFunction(): void { $view = new AppView(null, null, null, [ diff --git a/tests/test_app/templates/template_path_restriction.twig b/tests/test_app/templates/template_path_restriction.twig new file mode 100644 index 0000000..c672344 --- /dev/null +++ b/tests/test_app/templates/template_path_restriction.twig @@ -0,0 +1,5 @@ +{# +Ensure that `include` applies template path restriction, so that user data + include don't open +up arbitrary file reads. +#} +{% include item %} \ No newline at end of file