diff --git a/core/lib/Drupal/Core/Template/TwigEnvironment.php b/core/lib/Drupal/Core/Template/TwigEnvironment.php index dc7de16..200fd10 100644 --- a/core/lib/Drupal/Core/Template/TwigEnvironment.php +++ b/core/lib/Drupal/Core/Template/TwigEnvironment.php @@ -62,6 +62,10 @@ public function __construct($root, CacheBackendInterface $cache, \Twig_LoaderInt // Ensure autoescaping is always on. $options['autoescape'] = TRUE; + $policy = new TwigSandboxPolicy(); + $sandbox = new \Twig_Extension_Sandbox($policy, TRUE); + $this->addExtension($sandbox); + $this->loader = $loader; parent::__construct($this->loader, $options); } diff --git a/core/lib/Drupal/Core/Template/TwigSandboxPolicy.php b/core/lib/Drupal/Core/Template/TwigSandboxPolicy.php new file mode 100644 index 0000000..8e4ed0b --- /dev/null +++ b/core/lib/Drupal/Core/Template/TwigSandboxPolicy.php @@ -0,0 +1,52 @@ + TRUE, + 'label' => TRUE, + 'bundle' => TRUE, + 'get' => TRUE, + ]; + if (isset($whitelist[$method])) { + return TRUE; + } + + // If the method name starts with a whitelisted prefix, allow it. + if (preg_match('/^(get|has|is)[A-Za-z]/', $method)) { + return TRUE; + } + + throw new \Twig_Sandbox_SecurityError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, get_class($obj))); + } + } + +} diff --git a/core/modules/node/templates/node.html.twig b/core/modules/node/templates/node.html.twig index 5ed0775..d793f3a 100644 --- a/core/modules/node/templates/node.html.twig +++ b/core/modules/node/templates/node.html.twig @@ -4,12 +4,10 @@ * Default theme implementation to display a node. * * Available variables: - * - node: Full node entity. - * - id: The node ID. - * - bundle: The type of the node, for example, "page" or "article". - * - authorid: The user ID of the node author. - * - createdtime: Time the node was published formatted in Unix timestamp. - * - changedtime: Time the node was changed formatted in Unix timestamp. + * - node: The node entity with limited access to object properties and methods. + Only "getter" methods (method names starting with "get", "has", or "is") + and a few common attributes such as "id" and "label" are available. Calling + other methods (such as node.delete) will result in an exception. * - label: The title of the node. * - content: All node items. Use {{ content }} to print them all, * or print a subset such as {{ content.field_example }}. Use diff --git a/core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php b/core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php new file mode 100644 index 0000000..4a4259d --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php @@ -0,0 +1,122 @@ +addExtension($sandbox); + + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + + $caught = FALSE; + try { + $twig->render('{{ entity.delete }}', ['entity' => $entity]); + } catch (\Twig_Sandbox_SecurityError $e) { + $caught = TRUE; + } + $this->assertTrue($caught, '\Twig_sandbox_security exception was thrown when entity was tried to be removed.'); + + $caught = FALSE; + try { + $twig->render('{{ entity.save }}', ['entity' => $entity]); + } catch (\Twig_Sandbox_SecurityError $e) { + $caught = TRUE; + } + $this->assertTrue($caught, '\Twig_sandbox_security exception was thrown when entity was tried to be saved.'); + + $caught = FALSE; + try { + $twig->render('{{ entity.create }}', ['entity' => $entity]); + } catch (\Twig_Sandbox_SecurityError $e) { + $caught = TRUE; + } + $this->assertTrue($caught, '\Twig_sandbox_security exception was thrown when new entity was tried to be created.'); + } + + public function testEntitySafeMethods() { + $loader = new \Twig_Loader_String(); + $twig = new \Twig_Environment($loader); + $policy = new TwigSandboxPolicy(); + $sandbox = new \Twig_Extension_Sandbox($policy, TRUE); + $twig->addExtension($sandbox); + + // Test availability of get*, has*, and is* methods. + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->atLeastOnce()) + ->method('hasLinkTemplate') + ->with('test') + ->willReturn(TRUE); + $result = $twig->render('{{ entity.hasLinkTemplate("test") }}', ['entity' => $entity]); + $this->assertTrue((bool)$result, 'Sandbox policy allows has* functions to be called.'); + + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->atLeastOnce()) + ->method('isNew') + ->willReturn(TRUE); + $result = $twig->render('{{ entity.isNew }}', ['entity' => $entity]); + $this->assertTrue((bool)$result, 'Sandbox policy allows is* functions to be called.'); + + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->atLeastOnce()) + ->method('getEntityType') + ->willReturn('test'); + $result = $twig->render('{{ entity.getEntityType }}', ['entity' => $entity]); + $this->assertEquals($result, 'test', 'Sandbox policy allows get* functions to be called.'); + + // Test availability of whitelisted functions: id, label, bundle, and get. + $entity = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase') + ->disableOriginalConstructor() + ->getMock(); + $entity->expects($this->atLeastOnce()) + ->method('get') + ->with('title') + ->willReturn('test'); + $result = $twig->render('{{ entity.get("title") }}', ['entity' => $entity]); + $this->assertEquals($result, 'test', 'Sandbox policy allows get() to be called.'); + + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->atLeastOnce()) + ->method('id') + ->willReturn('1234'); + $result = $twig->render('{{ entity.id }}', ['entity' => $entity]); + $this->assertEquals($result, '1234', 'Sandbox policy allows get() to be called.'); + + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->atLeastOnce()) + ->method('label') + ->willReturn('testing'); + $result = $twig->render('{{ entity.label }}', ['entity' => $entity]); + $this->assertEquals($result, 'testing', 'Sandbox policy allows get() to be called.'); + + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->atLeastOnce()) + ->method('bundle') + ->willReturn('testing'); + $result = $twig->render('{{ entity.bundle }}', ['entity' => $entity]); + $this->assertEquals($result, 'testing', 'Sandbox policy allows get() to be called.'); + } + +} diff --git a/core/themes/bartik/templates/node.html.twig b/core/themes/bartik/templates/node.html.twig index 25144bf..974dfb4 100644 --- a/core/themes/bartik/templates/node.html.twig +++ b/core/themes/bartik/templates/node.html.twig @@ -4,12 +4,10 @@ * Bartik's theme implementation to display a node. * * Available variables: - * - node: Full node entity. - * - id: The node ID. - * - bundle: The type of the node, for example, "page" or "article". - * - authorid: The user ID of the node author. - * - createdtime: Time the node was published formatted in Unix timestamp. - * - changedtime: Time the node was changed formatted in Unix timestamp. + * - node: The node entity with limited access to object properties and methods. + Only "getter" methods (method names starting with "get", "has", or "is") + and a few common attributes such as "id" and "label" are available. Calling + other methods (such as node.delete) will result in an exception. * - label: The title of the node. * - content: All node items. Use {{ content }} to print them all, * or print a subset such as {{ content.field_example }}. Use diff --git a/core/themes/classy/templates/content/node.html.twig b/core/themes/classy/templates/content/node.html.twig index 5d746a6..5145064 100644 --- a/core/themes/classy/templates/content/node.html.twig +++ b/core/themes/classy/templates/content/node.html.twig @@ -4,12 +4,10 @@ * Theme override to display a node. * * Available variables: - * - node: Full node entity. - * - id: The node ID. - * - bundle: The type of the node, for example, "page" or "article". - * - authorid: The user ID of the node author. - * - createdtime: Time the node was published formatted in Unix timestamp. - * - changedtime: Time the node was changed formatted in Unix timestamp. + * - node: The node entity with limited access to object properties and methods. + Only "getter" methods (method names starting with "get", "has", or "is") + and a few common attributes such as "id" and "label" are available. Calling + other methods (such as node.delete) will result in an exception. * - label: The title of the node. * - content: All node items. Use {{ content }} to print them all, * or print a subset such as {{ content.field_example }}. Use