diff --git a/.github/workflows/tests.php b/.github/workflows/tests.php index 576b93b..73d3ded 100644 --- a/.github/workflows/tests.php +++ b/.github/workflows/tests.php @@ -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)); diff --git a/.gitignore b/.gitignore index 1fc0b65..5c91912 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /tmp/ /vendor/ /composer.lock +.DS_Store +.idea/ diff --git a/composer.json b/composer.json index ae68230..9675bd7 100644 --- a/composer.json +++ b/composer.json @@ -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" }, diff --git a/twig/twig/cve-2026-46627-sandbox-resource-exhaustion-docs.patch b/twig/twig/cve-2026-46627-sandbox-resource-exhaustion-docs.patch new file mode 100644 index 0000000..4c974b0 --- /dev/null +++ b/twig/twig/cve-2026-46627-sandbox-resource-exhaustion-docs.patch @@ -0,0 +1,20 @@ +From: Andrew Embler +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; diff --git a/twig/twig/cve-2026-46628-spaceless-pre-escape.patch b/twig/twig/cve-2026-46628-spaceless-pre-escape.patch new file mode 100644 index 0000000..34d268c --- /dev/null +++ b/twig/twig/cve-2026-46628-spaceless-pre-escape.patch @@ -0,0 +1,21 @@ +From fea4881a3e68d4cba6745bfd5d329e92d3205c3f Mon Sep 17 00:00:00 2001 +From: Andrew Embler +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) diff --git a/twig/twig/cve-2026-46633-use-template-name-code-injection.patch b/twig/twig/cve-2026-46633-use-template-name-code-injection.patch new file mode 100644 index 0000000..26eee1c --- /dev/null +++ b/twig/twig/cve-2026-46633-use-template-name-code-injection.patch @@ -0,0 +1,42 @@ +From 52ca23d8f4b7eba926693d3e2106c6a50eccb214 Mon Sep 17 00:00:00 2001 +From: Andrew Embler +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) diff --git a/twig/twig/cve-2026-46634-template-from-string-sandbox-caveats.patch b/twig/twig/cve-2026-46634-template-from-string-sandbox-caveats.patch new file mode 100644 index 0000000..7747fc8 --- /dev/null +++ b/twig/twig/cve-2026-46634-template-from-string-sandbox-caveats.patch @@ -0,0 +1,22 @@ +From 1fd298d76893a81f8bc3c13c20bea7a5b175cd92 Mon Sep 17 00:00:00 2001 +From: Andrew Embler +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) diff --git a/twig/twig/cve-2026-46638-sandbox-include-preloaded-template.patch b/twig/twig/cve-2026-46638-sandbox-include-preloaded-template.patch new file mode 100644 index 0000000..f55775e --- /dev/null +++ b/twig/twig/cve-2026-46638-sandbox-include-preloaded-template.patch @@ -0,0 +1,79 @@ +From 5038f40eab8e516493e33356d4e775e1f30b8195 Mon Sep 17 00:00:00 2001 +From: Andrew Embler +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) { ++ $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) diff --git a/twig/twig/cve-2026-47730-profiler-html-escaping.patch b/twig/twig/cve-2026-47730-profiler-html-escaping.patch new file mode 100644 index 0000000..3a8d7c5 --- /dev/null +++ b/twig/twig/cve-2026-47730-profiler-html-escaping.patch @@ -0,0 +1,36 @@ +From 76ae0de2438c78e1924686ee4fc9809d27ffcc89 Mon Sep 17 00:00:00 2001 +From: Andrew Embler +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└ %s', $prefix, self::$colors['template'], $profile->getTemplate()); ++ return \sprintf('%s└ %s', $prefix, self::$colors['template'], self::escape($profile->getTemplate())); + } + + protected function formatNonTemplate(Profile $profile, $prefix): string + { +- return \sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', $profile->getName()); ++ return \sprintf('%s└ %s::%s(%s)', $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('%.2fms/%.0f%%', $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)