Package: release.debian.org Severity: normal User: release.debian....@packages.debian.org Usertags: unblock
Please unblock package twig, it backports a security fix (Sandbox Information Disclosure) from the latest (2.7) version. https://symfony.com/blog/twig-sandbox-information-disclosure Unfortunately, upstream moved from PSR-0 to PSR-4 prior to fixing this security issue, so I had to backport the fix instead of simply cherry-pick the commit. I managed to backport the fixes of the testsuite too to help in the confidence that the fix is correct. 2.7 is in experimental, I can upload this version to unstable if you prefer. Ditto, upstream 1.38 moved from PSR-0 to PSR-4, and backporting the fix to 1.24 is even more tedious (some structures seem to have changed in between), so I’m not yet proposing a stretch-update (the security-team is X-Debbugs-CCed on this report, so they can share their point of view on this request). unblock twig/2.6.2-2 Thanks in advance. Regards David
diff --git a/debian/changelog b/debian/changelog index 60645e8a..446f5dfd 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +twig (2.6.2-2) unstable; urgency=medium + + * Team upload + * Stick to 2.6 for buster + * Backport fix from 2.7: security issue in the sandbox + + -- David Prévot <taf...@debian.org> Tue, 12 Mar 2019 10:35:44 -1000 + twig (2.6.2-1) unstable; urgency=medium * Team upload diff --git a/debian/gbp.conf b/debian/gbp.conf index cec628c7..f7127058 100644 --- a/debian/gbp.conf +++ b/debian/gbp.conf @@ -1,2 +1,3 @@ [DEFAULT] +debian-branch = buster pristine-tar = True diff --git a/debian/patches/0001-Fix-security-issue-in-the-sandbox.patch b/debian/patches/0001-Fix-security-issue-in-the-sandbox.patch new file mode 100644 index 00000000..7f872fc0 --- /dev/null +++ b/debian/patches/0001-Fix-security-issue-in-the-sandbox.patch @@ -0,0 +1,346 @@ +From: =?utf-8?q?David_Pr=C3=A9vot?= <da...@tilapin.org> +Date: Tue, 12 Mar 2019 10:13:15 -1000 +Subject: Fix security issue in the sandbox + +Fix sandbox security issue (under some circumstances, calling the +__toString() method on an object was possible even if not allowed by the +security policy). + +Origin: backport, https://github.com/twigphp/Twig/commit/eac5422956e1dcca89a3669a03a3ff32f0502077 +--- + lib/Twig/Node/CheckToString.php | 39 ++++++++++++ + lib/Twig/Node/SandboxedPrint.php | 2 + + lib/Twig/NodeVisitor/Sandbox.php | 45 +++++++++++++- + src/Node/CheckToStringNode.php | 11 ++++ + test/Twig/Tests/Extension/SandboxTest.php | 95 ++++++++++++++++++++--------- + test/Twig/Tests/Node/SandboxedPrintTest.php | 33 ---------- + 6 files changed, 160 insertions(+), 65 deletions(-) + create mode 100644 lib/Twig/Node/CheckToString.php + create mode 100644 src/Node/CheckToStringNode.php + delete mode 100644 test/Twig/Tests/Node/SandboxedPrintTest.php + +diff --git a/lib/Twig/Node/CheckToString.php b/lib/Twig/Node/CheckToString.php +new file mode 100644 +index 0000000..07a7837 +--- /dev/null ++++ b/lib/Twig/Node/CheckToString.php +@@ -0,0 +1,39 @@ ++<?php ++ ++/* ++ * This file is part of Twig. ++ * ++ * (c) Fabien Potencier ++ * ++ * For the full copyright and license information, please view the LICENSE ++ * file that was distributed with this source code. ++ */ ++ ++/** ++ * Checks if casting an expression to __toString() is allowed by the sandbox. ++ * ++ * For instance, when there is a simple Print statement, like {{ article }}, ++ * and if the sandbox is enabled, we need to check that the __toString() ++ * method is allowed if 'article' is an object. The same goes for {{ article|upper }} ++ * or {{ random(article) }} ++ * ++ * @author Fabien Potencier <fab...@symfony.com> ++ */ ++class Twig_Node_CheckToString extends Twig_Node ++{ ++ public function __construct(Twig_Node_Expression $expr) ++ { ++ parent::__construct(['expr' => $expr], [], $expr->getTemplateLine(), $expr->getNodeTag()); ++ } ++ ++ public function compile(Twig_Compiler $compiler) ++ { ++ $compiler ++ ->write('$this->extensions[\'Twig_Extension_Sandbox\']->ensureToStringAllowed(') ++ ->subcompile($this->getNode('expr')) ++ ->raw(')') ++ ; ++ } ++} ++ ++class_alias('Twig_Node_CheckToString', 'Twig\Node\CheckToStringNode', false); +diff --git a/lib/Twig/Node/SandboxedPrint.php b/lib/Twig/Node/SandboxedPrint.php +index eb45cb8..aee7d2f 100644 +--- a/lib/Twig/Node/SandboxedPrint.php ++++ b/lib/Twig/Node/SandboxedPrint.php +@@ -17,6 +17,8 @@ + * and if the sandbox is enabled, we need to check that the __toString() + * method is allowed if 'article' is an object. + * ++ * Not used anymore, to be deprecated in 2.x and removed in 3.0 ++ * + * @author Fabien Potencier <fab...@symfony.com> + */ + class Twig_Node_SandboxedPrint extends Twig_Node_Print +diff --git a/lib/Twig/NodeVisitor/Sandbox.php b/lib/Twig/NodeVisitor/Sandbox.php +index 4d41ff6..cdc7ff2 100644 +--- a/lib/Twig/NodeVisitor/Sandbox.php ++++ b/lib/Twig/NodeVisitor/Sandbox.php +@@ -21,6 +21,8 @@ final class Twig_NodeVisitor_Sandbox extends Twig_BaseNodeVisitor + private $filters; + private $functions; + ++ private $needsToStringWrap = false; ++ + protected function doEnterNode(Twig_Node $node, Twig_Environment $env) + { + if ($node instanceof Twig_Node_Module) { +@@ -51,9 +53,28 @@ final class Twig_NodeVisitor_Sandbox extends Twig_BaseNodeVisitor + $this->functions['range'] = $node; + } + +- // wrap print to check __toString() calls + if ($node instanceof Twig_Node_Print) { +- return new Twig_Node_SandboxedPrint($node->getNode('expr'), $node->getTemplateLine(), $node->getNodeTag()); ++ $this->needsToStringWrap = true; ++ $this->wrapNode($node, 'expr'); ++ } ++ ++ if ($node instanceof Twig_Node_Set && !$node->getAttribute('capture')) { ++ $this->needsToStringWrap = true; ++ } ++ ++ // wrap outer nodes that can implicitly call __toString() ++ if ($this->needsToStringWrap) { ++ if ($node instanceof Twig_Node_Expression_Binary_Concat) { ++ $this->wrapNode($node, 'left'); ++ $this->wrapNode($node, 'right'); ++ } ++ if ($node instanceof Twig_Node_Expression_Filter) { ++ $this->wrapNode($node, 'node'); ++ $this->wrapArrayNode($node, 'arguments'); ++ } ++ if ($node instanceof Twig_Node_Expression_Function) { ++ $this->wrapArrayNode($node, 'arguments'); ++ } + } + } + +@@ -66,11 +87,31 @@ final class Twig_NodeVisitor_Sandbox extends Twig_BaseNodeVisitor + $this->inAModule = false; + + $node->setNode('display_start', new Twig_Node([new Twig_Node_CheckSecurity($this->filters, $this->tags, $this->functions), $node->getNode('display_start')])); ++ } elseif ($this->inAModule) { ++ if ($node instanceof Twig_Node_Print || $node instanceof Twig_Node_Set) { ++ $this->needsToStringWrap = false; ++ } + } + + return $node; + } + ++ private function wrapNode(Twig_Node $node, $name) ++ { ++ $expr = $node->getNode($name); ++ if ($expr instanceof Twig_Node_Expression_Name || $expr instanceof Twig_Node_Expression_GetAttr) { ++ $node->setNode($name, new Twig_Node_CheckToString($expr)); ++ } ++ } ++ ++ private function wrapArrayNode(Twig_Node $node, $name) ++ { ++ $args = $node->getNode($name); ++ foreach ($args as $name => $_) { ++ $this->wrapNode($args, $name); ++ } ++ } ++ + public function getPriority() + { + return 0; +diff --git a/src/Node/CheckToStringNode.php b/src/Node/CheckToStringNode.php +new file mode 100644 +index 0000000..d8df055 +--- /dev/null ++++ b/src/Node/CheckToStringNode.php +@@ -0,0 +1,11 @@ ++<?php ++ ++namespace Twig\Node; ++ ++class_exists('Twig_Node_CheckToString'); ++ ++if (\false) { ++ class CheckToStringNode extends \Twig_Node_CheckToString ++ { ++ } ++} +diff --git a/test/Twig/Tests/Extension/SandboxTest.php b/test/Twig/Tests/Extension/SandboxTest.php +index aef39c3..deb4a3d 100644 +--- a/test/Twig/Tests/Extension/SandboxTest.php ++++ b/test/Twig/Tests/Extension/SandboxTest.php +@@ -28,7 +28,6 @@ class Twig_Tests_Extension_SandboxTest extends \PHPUnit\Framework\TestCase + '1_basic3' => '{% if name %}foo{% endif %}', + '1_basic4' => '{{ obj.bar }}', + '1_basic5' => '{{ obj }}', +- '1_basic6' => '{{ arr.obj }}', + '1_basic7' => '{{ cycle(["foo","bar"], 1) }}', + '1_basic8' => '{{ obj.getfoobar }}{{ obj.getFooBar }}', + '1_basic9' => '{{ obj.foobar }}{{ obj.fooBar }}', +@@ -106,11 +105,14 @@ class Twig_Tests_Extension_SandboxTest extends \PHPUnit\Framework\TestCase + } + } + +- public function testSandboxUnallowedToString() ++ /** ++ * @dataProvider getSandboxUnallowedToStringTests ++ */ ++ public function testSandboxUnallowedToString($template) + { +- $twig = $this->getEnvironment(true, [], self::$templates); ++ $twig = $this->getEnvironment(true, [], ['index' => $template], [], ['upper'], ['FooObject' => 'getAnotherFooObject'], [], ['random']); + try { +- $twig->loadTemplate('1_basic5')->render(self::$params); ++ $twig->loadTemplate('index')->render(self::$params); + $this->fail('Sandbox throws a SecurityError exception if an unallowed method (__toString()) is called in the template'); + } catch (Twig_Sandbox_SecurityError $e) { + $this->assertInstanceOf('Twig_Sandbox_SecurityNotAllowedMethodError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedMethodError'); +@@ -119,17 +121,61 @@ class Twig_Tests_Extension_SandboxTest extends \PHPUnit\Framework\TestCase + } + } + +- public function testSandboxUnallowedToStringArray() ++ public function getSandboxUnallowedToStringTests() + { +- $twig = $this->getEnvironment(true, [], self::$templates); +- try { +- $twig->loadTemplate('1_basic6')->render(self::$params); +- $this->fail('Sandbox throws a SecurityError exception if an unallowed method (__toString()) is called in the template'); +- } catch (Twig_Sandbox_SecurityError $e) { +- $this->assertInstanceOf('Twig_Sandbox_SecurityNotAllowedMethodError', $e, 'Exception should be an instance of Twig_Sandbox_SecurityNotAllowedMethodError'); +- $this->assertEquals('FooObject', $e->getClassName(), 'Exception should be raised on the "FooObject" class'); +- $this->assertEquals('__tostring', $e->getMethodName(), 'Exception should be raised on the "__toString" method'); +- } ++ return [ ++ 'simple' => ['{{ obj }}'], ++ 'object_from_array' => ['{{ arr.obj }}'], ++ 'object_chain' => ['{{ obj.anotherFooObject }}'], ++ 'filter' => ['{{ obj|upper }}'], ++ 'filter_from_array' => ['{{ arr.obj|upper }}'], ++ 'function' => ['{{ random(obj) }}'], ++ 'function_from_array' => ['{{ random(arr.obj) }}'], ++ 'function_and_filter' => ['{{ random(obj|upper) }}'], ++ 'function_and_filter_from_array' => ['{{ random(arr.obj|upper) }}'], ++ 'object_chain_and_filter' => ['{{ obj.anotherFooObject|upper }}'], ++ 'object_chain_and_function' => ['{{ random(obj.anotherFooObject) }}'], ++ 'concat' => ['{{ obj ~ "" }}'], ++ 'concat_again' => ['{{ "" ~ obj }}'], ++ ]; ++ } ++ ++ /** ++ * @dataProvider getSandboxAllowedToStringTests ++ */ ++ public function testSandboxAllowedToString($template, $output) ++ { ++ $twig = $this->getEnvironment(true, [], ['index' => $template], ['set'], [], ['FooObject' => ['foo', 'getAnotherFooObject']]); ++ $this->assertEquals($output, $twig->load('index')->render(self::$params)); ++ } ++ ++ public function getSandboxAllowedToStringTests() ++ { ++ return [ ++ 'constant_test' => ['{{ obj is constant("PHP_INT_MAX") }}', ''], ++ 'set_object' => ['{% set a = obj.anotherFooObject %}{{ a.foo }}', 'foo'], ++ 'is_defined' => ['{{ obj.anotherFooObject is defined }}', '1'], ++ 'is_null' => ['{{ obj is null }}', ''], ++ 'is_sameas' => ['{{ obj is same as(obj) }}', '1'], ++ 'is_sameas_from_array' => ['{{ arr.obj is same as(arr.obj) }}', '1'], ++ 'is_sameas_from_another_method' => ['{{ obj.anotherFooObject is same as(obj.anotherFooObject) }}', ''], ++ ]; ++ } ++ ++ public function testSandboxAllowMethodToString() ++ { ++ $twig = $this->getEnvironment(true, [], self::$templates, [], [], ['FooObject' => '__toString']); ++ FooObject::reset(); ++ $this->assertEquals('foo', $twig->load('1_basic5')->render(self::$params), 'Sandbox allow some methods'); ++ $this->assertEquals(1, FooObject::$called['__toString'], 'Sandbox only calls method once'); ++ } ++ ++ public function testSandboxAllowMethodToStringDisabled() ++ { ++ $twig = $this->getEnvironment(false, [], self::$templates); ++ FooObject::reset(); ++ $this->assertEquals('foo', $twig->load('1_basic5')->render(self::$params), 'Sandbox allows __toString when sandbox disabled'); ++ $this->assertEquals(1, FooObject::$called['__toString'], 'Sandbox only calls method once'); + } + + public function testSandboxUnallowedFunction() +@@ -164,22 +210,6 @@ class Twig_Tests_Extension_SandboxTest extends \PHPUnit\Framework\TestCase + $this->assertEquals(1, FooObject::$called['foo'], 'Sandbox only calls method once'); + } + +- public function testSandboxAllowMethodToString() +- { +- $twig = $this->getEnvironment(true, [], self::$templates, [], [], ['FooObject' => '__toString']); +- FooObject::reset(); +- $this->assertEquals('foo', $twig->loadTemplate('1_basic5')->render(self::$params), 'Sandbox allow some methods'); +- $this->assertEquals(1, FooObject::$called['__toString'], 'Sandbox only calls method once'); +- } +- +- public function testSandboxAllowMethodToStringDisabled() +- { +- $twig = $this->getEnvironment(false, [], self::$templates); +- FooObject::reset(); +- $this->assertEquals('foo', $twig->loadTemplate('1_basic5')->render(self::$params), 'Sandbox allows __toString when sandbox disabled'); +- $this->assertEquals(1, FooObject::$called['__toString'], 'Sandbox only calls method once'); +- } +- + public function testSandboxAllowFilter() + { + $twig = $this->getEnvironment(true, [], self::$templates, [], ['upper']); +@@ -319,4 +349,9 @@ class FooObject + + return 'foobar'; + } ++ ++ public function getAnotherFooObject() ++ { ++ return new self(); ++ } + } +diff --git a/test/Twig/Tests/Node/SandboxedPrintTest.php b/test/Twig/Tests/Node/SandboxedPrintTest.php +deleted file mode 100644 +index 269a461..0000000 +--- a/test/Twig/Tests/Node/SandboxedPrintTest.php ++++ /dev/null +@@ -1,33 +0,0 @@ +-<?php +- +-/* +- * This file is part of Twig. +- * +- * (c) Fabien Potencier +- * +- * For the full copyright and license information, please view the LICENSE +- * file that was distributed with this source code. +- */ +- +-class Twig_Tests_Node_SandboxedPrintTest extends Twig_Test_NodeTestCase +-{ +- public function testConstructor() +- { +- $node = new Twig_Node_SandboxedPrint($expr = new Twig_Node_Expression_Constant('foo', 1), 1); +- +- $this->assertEquals($expr, $node->getNode('expr')); +- } +- +- public function getTests() +- { +- $tests = []; +- +- $tests[] = [new Twig_Node_SandboxedPrint(new Twig_Node_Expression_Constant('foo', 1), 1), <<<EOF +-// line 1 +-echo \$this->extensions['Twig_Extension_Sandbox']->ensureToStringAllowed("foo"); +-EOF +- ]; +- +- return $tests; +- } +-} diff --git a/debian/patches/series b/debian/patches/series new file mode 100644 index 00000000..3dd5ef20 --- /dev/null +++ b/debian/patches/series @@ -0,0 +1 @@ +0001-Fix-security-issue-in-the-sandbox.patch
signature.asc
Description: PGP signature