CVE-2019-9942: Insufficient Information (NVD-CWE-noinfo)
value: A sandbox information disclosure exists in Twig before 1.38.0 and 2.x before 2.7.0 because, under some circumstances, it is possible to call the __toString() method on an object even if not allowed by the security policy in place.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 3.7
Published: 2019-03-23T15:29Z
Repository: https://github.com/twigphp/Twig
Language: PHP
Files changed: CHANGELOG, src/Node/CheckToStringNode.php, src/Node/SandboxedPrintNode.php, src/NodeVisitor/SandboxNodeVisitor.php, test/Twig/Tests/Extension/SandboxTest.php
Commit date: 2019-03-12 11:09:54 +0100
Commit message: fixed security issue in the sandbox

Diff: diff --git a/CHANGELOG b/CHANGELOG
index b5e117f4..778fc6a1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,7 +1,10 @@
 * 1.38.0 (2019-XX-XX)
 
+ * fixed sandbox security issue (under some circumstances, calling the
+   __toString() method on an object was possible even if not allowed by the
+   security policy)
  * fixed batch filter clobbers array keys when fill parameter is used
  * added preserveKeys support for the batch filter
  * fixed "embed" support when used from "template_from_string"
  * added the possibility to pass a TemplateWrapper to Twig\Environment::load()
  * improved the performance of the sandbox
diff --git a/src/Node/CheckToStringNode.php b/src/Node/CheckToStringNode.php
new file mode 100644
index 00000000..464c96fb
--- /dev/null
+++ b/src/Node/CheckToStringNode.php
@@ -0,0 +1,42 @@
+<?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.
+ */
+
+namespace Twig\Node;
+
+use Twig\Compiler;
+use Twig\Node\Expression\AbstractExpression;
+
+/**
+ * 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 <fabien@symfony.com>
+ */
+class CheckToStringNode extends Node
+{
+    public function __construct(AbstractExpression $expr)
+    {
+        parent::__construct(['expr' => $expr], [], $expr->getTemplateLine(), $expr->getNodeTag());
+    }
+
+    public function compile(Compiler $compiler)
+    {
+        $compiler
+            ->raw('$this->sandbox->ensureToStringAllowed(')
+            ->subcompile($this->getNode('expr'))
+            ->raw(')')
+        ;
+    }
+}
diff --git a/src/Node/SandboxedPrintNode.php b/src/Node/SandboxedPrintNode.php
index efb7b357..2359af91 100644
--- a/src/Node/SandboxedPrintNode.php
+++ b/src/Node/SandboxedPrintNode.php
@@ -20,10 +20,12 @@ use Twig\Node\Expression\FilterExpression;
  *
  * 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.
  *
+ * Not used anymore, to be deprecated in 2.x and removed in 3.0
+ *
  * @author Fabien Potencier <fabien@symfony.com>
  */
 class SandboxedPrintNode extends PrintNode
 {
     public function compile(Compiler $compiler)
diff --git a/src/NodeVisitor/SandboxNodeVisitor.php b/src/NodeVisitor/SandboxNodeVisitor.php
index 726d8750..c1d302a8 100644
--- a/src/NodeVisitor/SandboxNodeVisitor.php
+++ b/src/NodeVisitor/SandboxNodeVisitor.php
@@ -11,17 +11,21 @@
 
 namespace Twig\NodeVisitor;
 
 use Twig\Environment;
 use Twig\Node\CheckSecurityNode;
+use Twig\Node\CheckToStringNode;
+use Twig\Node\Expression\Binary\ConcatBinary;
 use Twig\Node\Expression\Binary\RangeBinary;
 use Twig\Node\Expression\FilterExpression;
 use Twig\Node\Expression\FunctionExpression;
+use Twig\Node\Expression\GetAttrExpression;
+use Twig\Node\Expression\NameExpression;
 use Twig\Node\ModuleNode;
 use Twig\Node\Node;
 use Twig\Node\PrintNode;
-use Twig\Node\SandboxedPrintNode;
+use Twig\Node\SetNode;
 
 /**
  * @final
  *
  * @author Fabien Potencier <fabien@symfony.com>
@@ -31,10 +35,12 @@ class SandboxNodeVisitor extends AbstractNodeVisitor
     protected $inAModule = false;
     protected $tags;
     protected $filters;
     protected $functions;
 
+    private $needsToStringWrap = false;
+
     protected function doEnterNode(Node $node, Environment $env)
     {
         if ($node instanceof ModuleNode) {
             $this->inAModule = true;
             $this->tags = [];
@@ -61,13 +67,32 @@ class SandboxNodeVisitor extends AbstractNodeVisitor
             // the .. operator is equivalent to the range() function
             if ($node instanceof RangeBinary && !isset($this->functions['range'])) {
                 $this->functions['range'] = $node;
             }
 
-            // wrap print to check __toString() calls
             if ($node instanceof PrintNode) {
-                return new SandboxedPrintNode($node->getNode('expr'), $node->getTemplateLine(), $node->getNodeTag());
+                $this->needsToStringWrap = true;
+                $this->wrapNode($node, 'expr');
+            }
+
+            if ($node instanceof SetNode && !$node->getAttribute('capture')) {
+                $this->needsToStringWrap = true;
+            }
+
+            // wrap outer nodes that can implicitly call __toString()
+            if ($this->needsToStringWrap) {
+                if ($node instanceof ConcatBinary) {
+                    $this->wrapNode($node, 'left');
+                    $this->wrapNode($node, 'right');
+                }
+                if ($node instanceof FilterExpression) {
+                    $this->wrapNode($node, 'node');
+                    $this->wrapArrayNode($node, 'arguments');
+                }
+                if ($node instanceof FunctionExpression) {
+                    $this->wrapArrayNode($node, 'arguments');
+                }
             }
         }
 
         return $node;
     }
@@ -76,15 +101,35 @@ class SandboxNodeVisitor extends AbstractNodeVisitor
     {
         if ($node instanceof ModuleNode) {
             $this->inAModule = false;
 
             $node->setNode('constructor_end', new Node([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('display_start')]));
+        } elseif ($this->inAModule) {
+            if ($node instanceof PrintNode || $node instanceof SetNode) {
+                $this->needsToStringWrap = false;
+            }
         }
 
         return $node;
     }
 
+    private function wrapNode(Node $node, $name)
+    {
+        $expr = $node->getNode($name);
+        if ($expr instanceof NameExpression || $expr instanceof GetAttrExpression) {
+            $node->setNode($name, new CheckToStringNode($expr));
+        }
+    }
+
+    private function wrapArrayNode(Node $node, $name)
+    {
+        $args = $node->getNode($name);
+        foreach ($args as $name => $_) {
+            $this->wrapNode($args, $name);
+        }
+    }
+
     public function getPriority()
     {
         return 0;
     }
 }
diff --git a/test/Twig/Tests/Extension/SandboxTest.php b/test/Twig/Tests/Extension/SandboxTest.php
index e554bfbc..de9ced02 100644
--- a/test/Twig/Tests/Extension/SandboxTest.php
+++ b/test/Twig/Tests/Extension/SandboxTest.php
@@ -32,11 +32,10 @@ class Twig_Tests_Extension_SandboxTest extends \PHPUnit\Framework\TestCase
             '1_basic1' => '{{ obj.foo }}',
             '1_basic2' => '{{ name|upper }}',
             '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 }}',
             '1_basic' => '{% if obj.foo %}{{ obj.foo|upper }}{% endif %}',
             '1_layout' => '{% block content %}{% endblock %}',
@@ -110,34 +109,81 @@ class Twig_Tests_Extension_SandboxTest extends \PHPUnit\Framework\TestCase
             $this->assertEquals('FooObject', $e->getClassName(), 'Exception should be raised on the "FooObject" class');
             $this->assertEquals('bar', $e->getPropertyName(), 'Exception should be raised on the "bar" property');
         }
     }
 
-    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->load('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 (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');
         }
     }
 
-    public function testSandboxUnallowedToStringArray()
+    public function getSandboxUnallowedToStringTests()
     {
-        $twig = $this->getEnvironment(true, [], self::$templates);
-        try {
-            $twig->load('1_basic6')->render(self::$params);
-            $this->fail('Sandbox throws a SecurityError exception if an unallowed method (__toString()) is called in the template');
-        } catch (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()
     {
         $twig = $this->getEnvironment(true, [], self::$templates);
@@ -168,26 +214,10 @@ class Twig_Tests_Extension_SandboxTest extends \PHPUnit\Framework\TestCase
         FooObject::reset();
         $this->assertEquals('foo', $twig->load('1_basic1')->render(self::$params), 'Sandbox allow some methods');
         $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->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 testSandboxAllowFilter()
     {
         $twig = $this->getEnvironment(true, [], self::$templates, [], ['upper']);
         $this->assertEquals('FABIEN', $twig->load('1_basic2')->render(self::$params), 'Sandbox allow some filters');
     }
@@ -324,6 +354,11 @@ class FooObject
     {
         ++self::$called['getFooBar'];
 
         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 14c11913..00000000
--- a/test/Twig/Tests/Node/SandboxedPrintTest.php
+++ /dev/null
@@ -1,42 +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.
- */
-
-use Twig\Node\Expression\ConstantExpression;
-use Twig\Node\Expression\NameExpression;
-use Twig\Node\SandboxedPrintNode;
-use Twig\Test\NodeTestCase;
-
-class Twig_Tests_Node_SandboxedPrintTest extends NodeTestCase
-{
-    public function testConstructor()
-    {
-        $node = new SandboxedPrintNode($expr = new ConstantExpression('foo', 1), 1);
-
-        $this->assertEquals($expr, $node->getNode('expr'));
-    }
-
-    public function getTests()
-    {
-        $tests[] = [new SandboxedPrintNode(new ConstantExpression('foo', 1), 1), <<<EOF
-// line 1
-echo "foo";
-EOF
-        ];
-
-        $tests[] = [new SandboxedPrintNode(new NameExpression('foo', 1), 1), <<<EOF
-// line 1
-echo \$this->env->getExtension('\Twig\Extension\SandboxExtension')->ensureToStringAllowed({$this->getVariableGetter('foo', false)});
-EOF
-        ];
-
-        return $tests;
-    }
-}

Vulnerable code: use Twig\Node\SandboxedPrintNode;
            // wrap print to check __toString() calls
                return new SandboxedPrintNode($node->getNode('expr'), $node->getTemplateLine(), $node->getNodeTag());
            '1_basic6' => '{{ arr.obj }}',
    public function testSandboxUnallowedToString()
        $twig = $this->getEnvironment(true, [], self::$templates);
            $twig->load('1_basic5')->render(self::$params);
    public function testSandboxUnallowedToStringArray()
        $twig = $this->getEnvironment(true, [], self::$templates);
        try {
            $twig->load('1_basic6')->render(self::$params);
            $this->fail('Sandbox throws a SecurityError exception if an unallowed method (__toString()) is called in the template');
        } catch (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');
        }
    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');
    }

<?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.
 */

use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\SandboxedPrintNode;
use Twig\Test\NodeTestCase;

class Twig_Tests_Node_SandboxedPrintTest extends NodeTestCase
{
    public function testConstructor()
    {
        $node = new SandboxedPrintNode($expr = new ConstantExpression('foo', 1), 1);

        $this->assertEquals($expr, $node->getNode('expr'));
    }

    public function getTests()
    {
        $tests[] = [new SandboxedPrintNode(new ConstantExpression('foo', 1), 1), <<<EOF
// line 1
echo "foo";
EOF
        ];

        $tests[] = [new SandboxedPrintNode(new NameExpression('foo', 1), 1), <<<EOF
// line 1
echo \$this->env->getExtension('\Twig\Extension\SandboxExtension')->ensureToStringAllowed({$this->getVariableGetter('foo', false)});
EOF
        ];

        return $tests;
    }
}
Fixed code:  * fixed sandbox security issue (under some circumstances, calling the
   __toString() method on an object was possible even if not allowed by the
   security policy)
<?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.
 */

namespace Twig\Node;

use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;

/**
 * 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 <fabien@symfony.com>
 */
class CheckToStringNode extends Node
{
    public function __construct(AbstractExpression $expr)
    {
        parent::__construct(['expr' => $expr], [], $expr->getTemplateLine(), $expr->getNodeTag());
    }

    public function compile(Compiler $compiler)
    {
        $compiler
            ->raw('$this->sandbox->ensureToStringAllowed(')
            ->subcompile($this->getNode('expr'))
            ->raw(')')
        ;
    }
}
 * Not used anymore, to be deprecated in 2.x and removed in 3.0
 *
use Twig\Node\CheckToStringNode;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\SetNode;
    private $needsToStringWrap = false;

                $this->needsToStringWrap = true;
                $this->wrapNode($node, 'expr');
            }

            if ($node instanceof SetNode && !$node->getAttribute('capture')) {
                $this->needsToStringWrap = true;
            }

            // wrap outer nodes that can implicitly call __toString()
            if ($this->needsToStringWrap) {
                if ($node instanceof ConcatBinary) {
                    $this->wrapNode($node, 'left');
                    $this->wrapNode($node, 'right');
                }
                if ($node instanceof FilterExpression) {
                    $this->wrapNode($node, 'node');
                    $this->wrapArrayNode($node, 'arguments');
                }
                if ($node instanceof FunctionExpression) {
                    $this->wrapArrayNode($node, 'arguments');
                }
        } elseif ($this->inAModule) {
            if ($node instanceof PrintNode || $node instanceof SetNode) {
                $this->needsToStringWrap = false;
            }
    private function wrapNode(Node $node, $name)
    {
        $expr = $node->getNode($name);
        if ($expr instanceof NameExpression || $expr instanceof GetAttrExpression) {
            $node->setNode($name, new CheckToStringNode($expr));
        }
    }

    private function wrapArrayNode(Node $node, $name)
    {
        $args = $node->getNode($name);
        foreach ($args as $name => $_) {
            $this->wrapNode($args, $name);
        }
    }

    /**
     * @dataProvider getSandboxUnallowedToStringTests
     */
    public function testSandboxUnallowedToString($template)
        $twig = $this->getEnvironment(true, [], ['index' => $template], [], ['upper'], ['FooObject' => 'getAnotherFooObject'], [], ['random']);
            $twig->loadTemplate('index')->render(self::$params);
    public function getSandboxUnallowedToStringTests()
        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 getAnotherFooObject()
    {
        return new self();
    }