mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-15 20:06:31 +00:00
Reject cross-environment template wrappers
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
* Add `TempestMarkdown` to use `tempest/markdown` as the `markdown_to_html` converter
|
||||
* Add the `include_only` function to render a template without giving it access to the current context
|
||||
* Add the `Twig\Sandbox\SandboxInterface` interface and `Twig\Sandbox\Sandbox` class to render untrusted templates through a dedicated, always-sandboxed environment crafted for it
|
||||
* Reject `TemplateWrapper` instances created by another `Environment`
|
||||
* Add the `Twig\Extension\SandboxBridgeExtension` to render sandboxed templates from trusted templates with an explicit output escaping strategy
|
||||
* Extract the sandbox runtime enforcement into a new internal `Twig\Sandbox\SecurityChecker` class used by compiled templates and `CoreExtension`
|
||||
* Mark `SandboxExtension` as internal, use `Twig\Sandbox\Sandbox` instead
|
||||
|
||||
+3
-1
@@ -359,6 +359,8 @@ class Environment
|
||||
public function load($name): TemplateWrapper
|
||||
{
|
||||
if ($name instanceof TemplateWrapper) {
|
||||
$name->unwrap($this);
|
||||
|
||||
return $name;
|
||||
}
|
||||
if ($name instanceof Template) {
|
||||
@@ -503,7 +505,7 @@ class Environment
|
||||
return new TemplateWrapper($this, $name);
|
||||
}
|
||||
if ($name instanceof TemplateWrapper) {
|
||||
return $name;
|
||||
return $this->load($name);
|
||||
}
|
||||
|
||||
if (1 !== $count && !$this->getLoader()->exists($name)) {
|
||||
|
||||
+12
-5
@@ -73,11 +73,15 @@ abstract class Template
|
||||
* This method is for internal use only and should never be called
|
||||
* directly.
|
||||
*
|
||||
* @return self|TemplateWrapper|false The parent template or false if there is no parent
|
||||
* @return self|false The parent template or false if there is no parent
|
||||
*/
|
||||
public function getParent(array $context): self|TemplateWrapper|false
|
||||
public function getParent(array $context): self|false
|
||||
{
|
||||
if (null !== $this->parent) {
|
||||
if ($this->parent instanceof TemplateWrapper) {
|
||||
$this->parent = $this->load($this->parent, -1);
|
||||
}
|
||||
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
@@ -99,7 +103,10 @@ abstract class Template
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($parent instanceof self || $parent instanceof TemplateWrapper) {
|
||||
if ($parent instanceof TemplateWrapper) {
|
||||
$parent = $this->load($parent, -1);
|
||||
}
|
||||
if ($parent instanceof self) {
|
||||
return $this->parents[$parent->getSourceContext()->getName()] = $parent;
|
||||
}
|
||||
|
||||
@@ -298,11 +305,11 @@ abstract class Template
|
||||
{
|
||||
try {
|
||||
if (\is_array($template)) {
|
||||
return $this->env->resolveTemplate($template)->unwrap();
|
||||
return $this->env->resolveTemplate($template)->unwrap($this->env);
|
||||
}
|
||||
|
||||
if ($template instanceof TemplateWrapper) {
|
||||
return $template->unwrap();
|
||||
return $template->unwrap($this->env);
|
||||
}
|
||||
|
||||
if ($template === $this->getTemplateName()) {
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
namespace Twig;
|
||||
|
||||
use Twig\Error\RuntimeError;
|
||||
|
||||
/**
|
||||
* Exposes a template to userland.
|
||||
*
|
||||
@@ -96,11 +98,13 @@ final class TemplateWrapper
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return Template
|
||||
*/
|
||||
public function unwrap()
|
||||
public function unwrap(Environment $env): Template
|
||||
{
|
||||
if ($this->env !== $env) {
|
||||
throw new RuntimeError(\sprintf('A "%s" can only be used with the "%s" that created it.', self::class, Environment::class));
|
||||
}
|
||||
|
||||
return $this->template;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,6 +235,6 @@ class CallMacroTest extends TestCase
|
||||
{
|
||||
$twig = new Environment(new ArrayLoader($templates));
|
||||
|
||||
return $twig->load('index')->unwrap();
|
||||
return $twig->load('index')->unwrap($twig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,17 @@ class EnvironmentTest extends TestCase
|
||||
$this->assertEquals('foo\u003Cbr\/\u0020\u003E foo\u003Cbr\/\u0020\u003E', $twig->render('js', ['bar' => 'foo<br/ >']));
|
||||
}
|
||||
|
||||
public function testRejectsTemplateWrapperFromAnotherEnvironment(): void
|
||||
{
|
||||
$foreign = new Environment(new ArrayLoader(['index' => 'foreign']));
|
||||
$twig = new Environment(new ArrayLoader());
|
||||
|
||||
$this->expectException(RuntimeError::class);
|
||||
$this->expectExceptionMessage('can only be used with the "Twig\\Environment" that created it');
|
||||
|
||||
$twig->load($foreign->load('index'));
|
||||
}
|
||||
|
||||
public function escapingStrategyCallback($name)
|
||||
{
|
||||
return $name;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
namespace Twig\Tests\Sandbox;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
use Twig\Error\RuntimeError;
|
||||
@@ -229,6 +230,34 @@ class SandboxTest extends TestCase
|
||||
$sandbox->render('index');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideForeignTemplateWrapperUsages
|
||||
*/
|
||||
#[DataProvider('provideForeignTemplateWrapperUsages')]
|
||||
public function testRejectsTemplateWrapperFromAnotherEnvironment(string $template, string $foreignTemplate, array $tags = [], array $functions = []): void
|
||||
{
|
||||
$foreign = self::env(['foreign' => $foreignTemplate]);
|
||||
$sandbox = new Sandbox(self::env(['index' => $template]), self::strictPolicy(tags: $tags, functions: $functions));
|
||||
|
||||
$this->expectException(RuntimeError::class);
|
||||
$this->expectExceptionMessage('can only be used with the "Twig\\Environment" that created it');
|
||||
|
||||
$sandbox->render('index', ['foreign' => $foreign->load('foreign')]);
|
||||
}
|
||||
|
||||
public static function provideForeignTemplateWrapperUsages(): iterable
|
||||
{
|
||||
yield 'include tag' => ['{% include foreign %}', 'foreign content', ['include']];
|
||||
yield 'include function' => ['{{ include(foreign) }}', 'foreign content', [], ['include']];
|
||||
yield 'include function fallback' => ['{{ include(["missing", foreign]) }}', 'foreign content', [], ['include']];
|
||||
yield 'include_only function' => ['{{ include_only(foreign) }}', 'foreign content', [], ['include_only']];
|
||||
yield 'extends tag' => ['{% extends foreign %}', 'foreign content', ['extends']];
|
||||
yield 'embed tag' => ['{% embed foreign %}{% endembed %}', 'foreign content', ['embed', 'extends']];
|
||||
yield 'import tag' => ['{% import foreign as macros %}{{ macros.foo() }}', '{% macro foo() %}foreign content{% endmacro %}', ['import']];
|
||||
yield 'from tag' => ['{% from foreign import foo %}{{ foo() }}', '{% macro foo() %}foreign content{% endmacro %}', ['from']];
|
||||
yield 'block function' => ['{{ block("content", foreign) }}', '{% block content %}foreign content{% endblock %}', [], ['block']];
|
||||
}
|
||||
|
||||
public function testTheExtendsTagMustBeAllowed(): void
|
||||
{
|
||||
$templates = [
|
||||
|
||||
@@ -26,10 +26,23 @@ use Twig\Environment;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
use Twig\Template;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class TemplateWrapperTest extends TestCase
|
||||
{
|
||||
public function testUnwrapChecksTheEnvironment(): void
|
||||
{
|
||||
$twig = new Environment(new ArrayLoader(['index' => 'content']));
|
||||
$wrapper = $twig->load('index');
|
||||
|
||||
$this->assertInstanceOf(Template::class, $wrapper->unwrap($twig));
|
||||
|
||||
$this->expectException(RuntimeError::class);
|
||||
$this->expectExceptionMessage('can only be used with the "Twig\\Environment" that created it');
|
||||
$wrapper->unwrap(new Environment(new ArrayLoader()));
|
||||
}
|
||||
|
||||
public function testHasGetBlocks(): void
|
||||
{
|
||||
$twig = new Environment(new ArrayLoader([
|
||||
|
||||
Reference in New Issue
Block a user