Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ function createComposerJson(string $dir, string $packageName, string $packageVer
],
'config' => [
'allow-plugins' => true,
'audit' => [
// These test fixtures intentionally install vulnerable package
// versions so we can verify that the local patches still apply.
'block-insecure' => false,
],
],
];
file_put_contents($dir . '/composer.json', json_encode($composerConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/tmp/
/vendor/
/composer.lock
.DS_Store
.idea/
8 changes: 8 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@
"Fix continue switch in Minifier": "tedivm/jshrink/fix-minifier-loop.patch",
"Update to upstream version 1.3.2": "tedivm/jshrink/update-upstream-1.3.2.patch"
},
"twig/twig:3.11.3": {
"Document template_from_string() sandbox caveats (CVE-2026-46634)": "twig/twig/cve-2026-46634-template-from-string-sandbox-caveats.patch",
"Document sandbox resource exhaustion limits (CVE-2026-46627)": "twig/twig/cve-2026-46627-sandbox-resource-exhaustion-docs.patch",
"Pre-escape HTML input on the spaceless filter (CVE-2026-46628)": "twig/twig/cve-2026-46628-spaceless-pre-escape.patch",
"Fix code injection via use template names (CVE-2026-46633)": "twig/twig/cve-2026-46633-use-template-name-code-injection.patch",
"Escape profiler HTML output names (CVE-2026-47730)": "twig/twig/cve-2026-47730-profiler-html-escaping.patch",
"Fix sandbox includes with preloaded templates (CVE-2026-46638)": "twig/twig/cve-2026-46638-sandbox-include-preloaded-template.patch"
},
"wikimedia/less.php:1.8.2": {
"Add PHP 8.2 compatibility": "wikimedia/less.php/fix-php-8.2-compatibility.patch"
},
Expand Down
20 changes: 20 additions & 0 deletions twig/twig/cve-2026-46627-sandbox-resource-exhaustion-docs.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
From: Andrew Embler <andrew@concrete5.org>
Date: Tue, 26 May 2026 13:40:19 -0700
Subject: [PATCH] Document sandbox resource exhaustion limits


--- a/src/Extension/SandboxExtension.php
+++ b/src/Extension/SandboxExtension.php
@@ -17,6 +17,13 @@ use Twig\Source;
use Twig\TokenParser\SandboxTokenParser;

+/**
+ * Adds sandboxing for untrusted templates.
+ *
+ * Note: the sandbox restricts access to code and data, but it does not limit
+ * CPU, memory, or execution time. Contain untrusted templates at the process
+ * level if resource exhaustion matters.
+ */
final class SandboxExtension extends AbstractExtension
{
private $sandboxedGlobally;
21 changes: 21 additions & 0 deletions twig/twig/cve-2026-46628-spaceless-pre-escape.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
From fea4881a3e68d4cba6745bfd5d329e92d3205c3f Mon Sep 17 00:00:00 2001
From: Andrew Embler <andrew@concrete5.org>
Date: Tue, 26 May 2026 13:42:53 -0700
Subject: [PATCH] Pre-escape HTML input on the spaceless filter


diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php
index b077e796..76f5575e 100644
--- a/src/Extension/CoreExtension.php
+++ b/src/Extension/CoreExtension.php
@@ -222,7 +222,7 @@ final class CoreExtension extends AbstractExtension
new TwigFilter('striptags', [self::class, 'striptags']),
new TwigFilter('trim', [self::class, 'trim']),
new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
- new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html']]),
+ new TwigFilter('spaceless', [self::class, 'spaceless'], ['pre_escape' => 'html', 'is_safe' => ['html']]),

// array helpers
new TwigFilter('join', [self::class, 'join']),
--
2.50.1 (Apple Git-155)
42 changes: 42 additions & 0 deletions twig/twig/cve-2026-46633-use-template-name-code-injection.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
From 52ca23d8f4b7eba926693d3e2106c6a50eccb214 Mon Sep 17 00:00:00 2001
From: Andrew Embler <andrew@concrete5.org>
Date: Tue, 26 May 2026 13:44:39 -0700
Subject: [PATCH] Fix code injection via use template names


diff --git a/src/Compiler.php b/src/Compiler.php
index 1e7ed04c..ead8f929 100644
--- a/src/Compiler.php
+++ b/src/Compiler.php
@@ -144,7 +144,9 @@ class Compiler
*/
public function string(string $value)
{
- $this->source .= \sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));
+ // Defense in depth: avoid emitting literal single quotes derived from
+ // user input into compiled PHP source.
+ $this->source .= \sprintf('"%s"', str_replace("'", '\\x27', addcslashes($value, "\0\t\"\$\\")));

return $this;
}
diff --git a/src/Node/ModuleNode.php b/src/Node/ModuleNode.php
index fb85cd89..9feed91f 100644
--- a/src/Node/ModuleNode.php
+++ b/src/Node/ModuleNode.php
@@ -216,11 +216,11 @@ final class ModuleNode extends Node
->string($key)
->raw("])) {\n")
->indent()
- ->write("throw new RuntimeError('Block ")
+ ->write("throw new RuntimeError(sprintf('Block \"%s\" is not defined in trait \"%s\".', ")
->string($key)
- ->raw(' is not defined in trait ')
+ ->raw(', ')
->subcompile($trait->getNode('template'))
- ->raw(".', ")
+ ->raw('), ')
->repr($node->getTemplateLine())
->raw(", \$this->source);\n")
->outdent()
--
2.50.1 (Apple Git-155)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
From 1fd298d76893a81f8bc3c13c20bea7a5b175cd92 Mon Sep 17 00:00:00 2001
From: Andrew Embler <andrew@concrete5.org>
Date: Tue, 26 May 2026 13:40:14 -0700
Subject: [PATCH] Document template_from_string sandbox caveats


diff --git a/src/Extension/StringLoaderExtension.php b/src/Extension/StringLoaderExtension.php
index 12f5c30a..8f20c209 100644
--- a/src/Extension/StringLoaderExtension.php
+++ b/src/Extension/StringLoaderExtension.php
@@ -29,6 +29,9 @@ final class StringLoaderExtension extends AbstractExtension
*
* {{ include(template_from_string("Hello {{ name }}")) }}
*
+ * Never expose `template_from_string` to untrusted template
+ * authors (like in a sandboxed environment). See the docs for more details.
+ *
* @param string $template A template as a string or object implementing __toString()
* @param string|null $name An optional name of the template to be used in error messages
*
--
2.50.1 (Apple Git-155)
79 changes: 79 additions & 0 deletions twig/twig/cve-2026-46638-sandbox-include-preloaded-template.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
From 5038f40eab8e516493e33356d4e775e1f30b8195 Mon Sep 17 00:00:00 2001
From: Andrew Embler <andrew@concrete5.org>
Date: Tue, 26 May 2026 13:55:17 -0700
Subject: [PATCH] Fix sandbox includes with preloaded templates


diff --git a/src/Node/IncludeNode.php b/src/Node/IncludeNode.php
index 7073fa4a..5359dd85 100644
--- a/src/Node/IncludeNode.php
+++ b/src/Node/IncludeNode.php
@@ -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);
diff --git a/src/TokenParser/SandboxTokenParser.php b/src/TokenParser/SandboxTokenParser.php
index c919556e..de3f0fe1 100644
--- a/src/TokenParser/SandboxTokenParser.php
+++ b/src/TokenParser/SandboxTokenParser.php
@@ -39,7 +39,9 @@ final class SandboxTokenParser extends AbstractTokenParser
$stream->expect(/* Token::BLOCK_END_TYPE */ 3);

// in a sandbox tag, only include tags are allowed
- if (!$body instanceof IncludeNode) {
+ if ($body instanceof IncludeNode) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double check: really invert logic?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, as crazy as it looks that's part of the original commit

twigphp/Twig@1a023fe

+ $body->setAttribute('sandboxed', true);
+ } else {
foreach ($body as $node) {
if ($node instanceof TextNode && ctype_space($node->getAttribute('data'))) {
continue;
@@ -48,6 +50,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);
}
}

--
2.50.1 (Apple Git-155)
36 changes: 36 additions & 0 deletions twig/twig/cve-2026-47730-profiler-html-escaping.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
From 76ae0de2438c78e1924686ee4fc9809d27ffcc89 Mon Sep 17 00:00:00 2001
From: Andrew Embler <andrew@concrete5.org>
Date: Tue, 26 May 2026 13:45:12 -0700
Subject: [PATCH] Escape profiler HTML output names


diff --git a/src/Profiler/Dumper/HtmlDumper.php b/src/Profiler/Dumper/HtmlDumper.php
index cdab2de5..a5e59e49 100644
--- a/src/Profiler/Dumper/HtmlDumper.php
+++ b/src/Profiler/Dumper/HtmlDumper.php
@@ -32,16 +32,21 @@ final class HtmlDumper extends BaseDumper

protected function formatTemplate(Profile $profile, $prefix): string
{
- return \sprintf('%s└ <span style="background-color: %s">%s</span>', $prefix, self::$colors['template'], $profile->getTemplate());
+ return \sprintf('%s└ <span style="background-color: %s">%s</span>', $prefix, self::$colors['template'], self::escape($profile->getTemplate()));
}

protected function formatNonTemplate(Profile $profile, $prefix): string
{
- return \sprintf('%s└ %s::%s(<span style="background-color: %s">%s</span>)', $prefix, $profile->getTemplate(), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', $profile->getName());
+ return \sprintf('%s└ %s::%s(<span style="background-color: %s">%s</span>)', $prefix, self::escape($profile->getTemplate()), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', self::escape($profile->getName()));
}

protected function formatTime(Profile $profile, $percent): string
{
return \sprintf('<span style="color: %s">%.2fms/%.0f%%</span>', $percent > 20 ? self::$colors['big'] : 'auto', $profile->getDuration() * 1000, $percent);
}
+
+ private static function escape(string $value): string
+ {
+ return htmlspecialchars($value, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
+ }
}
--
2.50.1 (Apple Git-155)
Loading