security #cve-2026-46638 Fix sandbox bypass in the {% sandbox %} tag when including a preloaded template (alexandre-daubois)

This PR was merged into the twig-3.x branch.
This commit is contained in:
Fabien Potencier
2026-05-19 23:02:22 +02:00
3 changed files with 51 additions and 2 deletions
+20 -1
View File
@@ -38,6 +38,8 @@ class IncludeNode extends Node implements NodeOutputInterface
{
$compiler->addDebugInfo($this);
$sandboxed = $this->hasAttribute('sandboxed') && $this->getAttribute('sandboxed');
if ($this->getAttribute('ignore_missing')) {
$template = $compiler->getVarName();
@@ -60,15 +62,32 @@ class IncludeNode extends Node implements NodeOutputInterface
->write("}\n")
->write(\sprintf("if ($%s) {\n", $template))
->indent()
->write(\sprintf('yield from $%s->unwrap()->yield(', $template))
;
if ($sandboxed) {
$compiler->write(\sprintf("\$%s->unwrap()->checkSecurity();\n", $template));
}
$compiler->write(\sprintf('yield from $%s->unwrap()->yield(', $template));
$this->addTemplateArguments($compiler);
$compiler
->raw(");\n")
->outdent()
->write("}\n")
;
} elseif ($sandboxed) {
$template = $compiler->getVarName();
$compiler->write(\sprintf('$%s = ', $template));
$this->addGetTemplate($compiler);
$compiler
->raw(";\n")
->write(\sprintf("\$%s->unwrap()->checkSecurity();\n", $template))
->write(\sprintf('yield from $%s->unwrap()->yield(', $template))
;
$this->addTemplateArguments($compiler);
$compiler->raw(");\n");
} else {
$compiler->write('yield from ');
$this->addGetTemplate($compiler);
+5 -1
View File
@@ -41,7 +41,9 @@ final class SandboxTokenParser extends AbstractTokenParser
$stream->expect(Token::BLOCK_END_TYPE);
// in a sandbox tag, only include tags are allowed
if (!$body instanceof IncludeNode) {
if ($body instanceof IncludeNode) {
$body->setAttribute('sandboxed', true);
} else {
foreach ($body as $node) {
if ($node instanceof TextNode && ctype_space($node->getAttribute('data'))) {
continue;
@@ -50,6 +52,8 @@ final class SandboxTokenParser extends AbstractTokenParser
if (!$node instanceof IncludeNode) {
throw new SyntaxError('Only "include" tags are allowed within a "sandbox" section.', $node->getTemplateLine(), $stream->getSourceContext());
}
$node->setAttribute('sandboxed', true);
}
}
+26
View File
@@ -535,6 +535,32 @@ EOF
$this->assertFalse($twig->getExtension(SandboxExtension::class)->isSandboxed(), 'Sandboxed include() function call should not leave Sandbox enabled when an error occurs.');
}
public function testSandboxTagIncludeWithPreloadedTemplate()
{
$twig = $this->getEnvironment(false, [], [
'index' => '{% sandbox %}{% include "included" %}{% endsandbox %}',
'included' => '{{ "hello"|upper }}',
]);
$twig->load('included');
$this->expectException(SecurityNotAllowedFilterError::class);
$twig->load('index')->render([]);
}
public function testSandboxTagIncludeIgnoreMissingWithPreloadedTemplate()
{
$twig = $this->getEnvironment(false, [], [
'index' => '{% sandbox %}{% include "included" ignore missing %}{% endsandbox %}',
'included' => '{{ "hello"|upper }}',
]);
$twig->load('included');
$this->expectException(SecurityNotAllowedFilterError::class);
$twig->load('index')->render([]);
}
public function testSandboxWithNoClosureFilter()
{
$twig = $this->getEnvironment(true, ['autoescape' => 'html'], ['index' => <<<EOF