mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-14 03:16:34 +00:00
security #cve-2026-46633 Fix sandbox bypass: PHP code injection via {% use %} template name (alexandre-daubois, fabpot)
This PR was merged into the twig-3.x branch.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 3.26.0 (2026-XX-XX)
|
||||
|
||||
* n/a
|
||||
* Encode single quotes as `\x27` in `Compiler::string()` as a defense-in-depth measure
|
||||
|
||||
# 3.25.0 (2026-05-17)
|
||||
|
||||
|
||||
+7
-1
@@ -143,7 +143,13 @@ class Compiler
|
||||
*/
|
||||
public function string(string $value)
|
||||
{
|
||||
$this->source .= \sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));
|
||||
// Single quotes are encoded as \x27 (not \') as a defense-in-depth measure:
|
||||
// it guarantees that the compiled output never contains a literal "'" derived
|
||||
// from user input, which prevents breaking out of a surrounding single-quoted
|
||||
// PHP context if a caller mistakenly concatenates the result into one.
|
||||
// \' is not a recognized escape sequence in PHP double-quoted strings (the
|
||||
// backslash would be kept literally), so \x27 is used instead.
|
||||
$this->source .= \sprintf('"%s"', str_replace("'", '\\x27', addcslashes($value, "\0\t\"\$\\")));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -248,11 +248,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()
|
||||
|
||||
@@ -27,6 +27,24 @@ use Twig\Loader\ArrayLoader;
|
||||
|
||||
class CompilerTest extends TestCase
|
||||
{
|
||||
public function testStringEncodesSingleQuotesAsHexEscape()
|
||||
{
|
||||
$compiler = new Compiler(new Environment(new ArrayLoader()));
|
||||
|
||||
// Defense in depth: a single quote in the source value must NOT appear as a
|
||||
// literal "'" in the compiled output, so that even if a caller mistakenly
|
||||
// concatenates the result into a single-quoted PHP string, the value cannot
|
||||
// break out of that context. It must still decode back to the original byte.
|
||||
$source = $compiler->string("it's \"a\" test")->getSource();
|
||||
|
||||
$this->assertStringNotContainsString("'", $source);
|
||||
$this->assertSame('"it\\x27s \\"a\\" test"', $source);
|
||||
|
||||
$decoded = null;
|
||||
eval('$decoded = '.$source.';');
|
||||
$this->assertSame("it's \"a\" test", $decoded);
|
||||
}
|
||||
|
||||
public function testReprNumericValueWithLocale()
|
||||
{
|
||||
$compiler = new Compiler(new Environment(new ArrayLoader()));
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Twig\Tests\Node;
|
||||
*/
|
||||
|
||||
use Twig\Environment;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
use Twig\Node\BodyNode;
|
||||
use Twig\Node\EmptyNode;
|
||||
@@ -56,6 +57,29 @@ class ModuleTest extends NodeTestCase
|
||||
$this->assertEquals($source->getName(), $node->getTemplateName());
|
||||
}
|
||||
|
||||
public function testUseTagTemplateNameDoesNotInjectPhpInCompiledOutput()
|
||||
{
|
||||
$evilName = "evil' . print('BAD-EOL') . '.twig";
|
||||
$loader = new ArrayLoader([
|
||||
$evilName => '{% block existing %}ok{% endblock %}',
|
||||
'main.twig' => "{% use \"$evilName\" with absent_block as alias %}",
|
||||
]);
|
||||
$twig = new Environment($loader);
|
||||
|
||||
ob_start();
|
||||
$message = null;
|
||||
try {
|
||||
$twig->load('main.twig');
|
||||
} catch (RuntimeError $e) {
|
||||
$message = $e->getMessage();
|
||||
}
|
||||
$stdout = ob_get_clean();
|
||||
|
||||
$this->assertSame('', $stdout, 'No code from the template name must execute when the trait is loaded.');
|
||||
$this->assertNotNull($message, 'A RuntimeError must be raised for the missing block.');
|
||||
$this->assertStringContainsString($evilName, $message, 'The error message must contain the literal template name.');
|
||||
}
|
||||
|
||||
public static function provideTests(): iterable
|
||||
{
|
||||
$twig = new Environment(new ArrayLoader(['foo.twig' => '{{ foo }}']));
|
||||
|
||||
Reference in New Issue
Block a user