diff --git a/core/lib/Drupal/Core/Template/TwigEnvironment.php b/core/lib/Drupal/Core/Template/TwigEnvironment.php index eb4f8e5..3a3d002 100644 --- a/core/lib/Drupal/Core/Template/TwigEnvironment.php +++ b/core/lib/Drupal/Core/Template/TwigEnvironment.php @@ -58,6 +58,10 @@ public function __construct($root, CacheBackendInterface $cache, $twig_extension // Ensure autoescaping is always on. $options['autoescape'] = 'html'; + $policy = new TwigSandboxPolicy(); + $sandbox = new \Twig_Extension_Sandbox($policy, TRUE); + $this->addExtension($sandbox); + if ($options['cache'] === TRUE) { $options['cache'] = new TwigPhpStorageCache($cache, $twig_extension_hash); } diff --git a/core/lib/Drupal/Core/Template/TwigSandboxPolicy.php b/core/lib/Drupal/Core/Template/TwigSandboxPolicy.php new file mode 100644 index 0000000..4313e7f --- /dev/null +++ b/core/lib/Drupal/Core/Template/TwigSandboxPolicy.php @@ -0,0 +1,101 @@ + TRUE. + */ + protected $whitelisted_methods = NULL; + + /** + * An array of whitelisted method prefixes -- any method starting with one of + * these prefixes will be allowed. + */ + protected $whitelisted_prefixes = NULL; + + /** + * An array of class names for which any method calls are allowed. + */ + protected $whitelisted_classes = NULL; + + public function __construct() { + // Allow settings.php to override our default whitelisted classes, methods, + // and prefixes. + $whitelisted_classes = Settings::get('twig_sandbox_whitelisted_classes', [ + // Allow any operations on the Attribute object as it is intended to be + // changed from a Twig template, for example calling addClass(). + 'Drupal\Core\Template\Attribute', + ]); + // Flip the arrays so we can check using isset(). + $this->whitelisted_classes = array_flip($whitelisted_classes); + + $whitelisted_methods = Settings::get('twig_sandbox_whitelisted_methods', [ + // Only allow idempotent methods. + 'id', + 'label', + 'bundle', + 'get', + '__toString', + ]); + $this->whitelisted_methods = array_flip($whitelisted_methods); + + $this->whitelisted_prefixes = Settings::get('twig_sandbox_whitelisted_prefixes', [ + 'get', + 'has', + 'is', + ]); + } + + /** + * {@inheritdoc} + */ + public function checkSecurity($tags, $filters, $functions) {} + + /** + * {@inheritdoc} + */ + public function checkPropertyAllowed($obj, $property) {} + + /** + * {@inheritdoc} + */ + public function checkMethodAllowed($obj, $method) { + if (isset($this->whitelisted_classes[get_class($obj)])) { + return TRUE; + } + + // Return quickly for an exact match of the method name. + if (isset($this->whitelisted_methods[$method])) { + return TRUE; + } + + // If the method name starts with a whitelisted prefix, allow it. + // Note: strpos() is between 3x and 7x faster than preg_match in this case. + foreach ($this->whitelisted_prefixes as $prefix) { + if (strpos($method, $prefix) === 0) { + 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..e7e353d 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 methods 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/modules/system/src/Tests/Theme/TwigWhiteListTest.php b/core/modules/system/src/Tests/Theme/TwigWhiteListTest.php new file mode 100644 index 0000000..a48debe --- /dev/null +++ b/core/modules/system/src/Tests/Theme/TwigWhiteListTest.php @@ -0,0 +1,130 @@ +installSchema('system', array('router', 'sequences')); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installEntitySchema('taxonomy_term'); + NodeType::create([ + 'type' => 'page', + 'name' => 'Basic page', + 'display_submitted' => FALSE, + ])->save(); + // Add a vocabulary so we can test different view modes. + $vocabulary = Vocabulary::create([ + 'name' => $this->randomMachineName(), + 'description' => $this->randomMachineName(), + 'vid' => $this->randomMachineName(), + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + 'help' => '', + ]); + $vocabulary->save(); + + // Add a term to the vocabulary. + $this->term = Term::create([ + 'name' => 'Sometimes people are just jerks', + 'description' => $this->randomMachineName(), + 'vid' => $vocabulary->id(), + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ]); + $this->term->save(); + + // Create a field. + $handler_settings = array( + 'target_bundles' => array( + $vocabulary->id() => $vocabulary->id(), + ), + 'auto_create' => TRUE, + ); + // Add the term field. + FieldStorageConfig::create(array( + 'field_name' => 'field_term', + 'type' => 'entity_reference', + 'entity_type' => 'node', + 'cardinality' => 1, + 'settings' => array( + 'target_type' => 'taxonomy_term', + ), + ))->save(); + FieldConfig::create(array( + 'field_name' => 'field_term', + 'entity_type' => 'node', + 'bundle' => 'page', + 'label' => 'Terms', + 'settings' => array( + 'handler' => 'default', + 'handler_settings' => $handler_settings, + ), + ))->save(); + + // Show on default display and teaser. + entity_get_display('node', 'page', 'default') + ->setComponent('field_term', array( + 'type' => 'entity_reference_label', + )) + ->save(); + // Boot twig environment. + $this->twig = \Drupal::service('twig'); + } + + /** + * Tests white-listing of methods doesn't interfere with chaining. + */ + public function testWhiteListChaining() { + $node = Node::create([ + 'type' => 'page', + 'title' => 'Some node mmk', + 'status' => 1, + 'field_term' => $this->term->id(), + ]); + $node->save(); + $this->setRawContent(twig_render_template(drupal_get_path('theme', 'test_theme') . '/templates/node.html.twig', ['node' => $node])); + $this->assertText('Sometimes people are just jerks'); + } + +} diff --git a/core/modules/system/tests/themes/test_theme/templates/node.html.twig b/core/modules/system/tests/themes/test_theme/templates/node.html.twig new file mode 100644 index 0000000..be94984 --- /dev/null +++ b/core/modules/system/tests/themes/test_theme/templates/node.html.twig @@ -0,0 +1,13 @@ +{# +/** + * @file + * Minimal template to ensure chained property access works with white-listing. + */ +#} + + +
+ {{ node.field_term.entity.label }} +
+ + 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..80eed7a --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php @@ -0,0 +1,135 @@ +twig = new \Twig_Environment($loader); + $policy = new TwigSandboxPolicy(); + $sandbox = new \Twig_Extension_Sandbox($policy, TRUE); + $this->twig->addExtension($sandbox); + } + + /** + * Tests that dangerous methods cannot be called in entity objects. + * + * @dataProvider getTwigEntityDangerousMethods + * @expectedException \Twig_Sandbox_SecurityError + */ + public function testEntityDangerousMethods($template) { + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $this->twig->render($template, ['entity' => $entity]); + } + + /** + * Data provider for ::testEntityDangerousMethods. + * + * @return array + */ + public function getTwigEntityDangerousMethods() { + return [ + ['{{ entity.delete }}'], + ['{{ entity.save }}'], + ['{{ entity.create }}'], + ]; + } + + /** + * Tests that prefixed methods can be called from within Twig templates. + * + * Currently "get", "has", and "is" are the only allowed prefixes. + */ + public function testEntitySafePrefixes() { + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->atLeastOnce()) + ->method('hasLinkTemplate') + ->with('test') + ->willReturn(TRUE); + $result = $this->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 = $this->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 = $this->twig->render('{{ entity.getEntityType }}', ['entity' => $entity]); + $this->assertEquals($result, 'test', 'Sandbox policy allows get* functions to be called.'); + } + + /** + * Tests that valid methods can be called from within Twig templates. + * + * Currently the following methods are whitelisted: id, label, bundle, and + * get. + */ + public function testEntitySafeMethods() { + $entity = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase') + ->disableOriginalConstructor() + ->getMock(); + $entity->expects($this->atLeastOnce()) + ->method('get') + ->with('title') + ->willReturn('test'); + $result = $this->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 = $this->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 = $this->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 = $this->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 754b670..5d509aa 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 methods 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 dbef76d..5af7c25 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 methods 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