diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php index 735a434..906af7f 100644 --- a/core/lib/Drupal/Core/Template/TwigExtension.php +++ b/core/lib/Drupal/Core/Template/TwigExtension.php @@ -15,6 +15,7 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\SafeStringInterface; +use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Render\RenderableInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Routing\UrlGeneratorInterface; @@ -134,6 +135,7 @@ public function getFilters() { // be used in "trans" tags. // @see TwigNodeTrans::compileString() new \Twig_SimpleFilter('passthrough', 'twig_raw_filter', array('is_safe' => array('html'))), + new \Twig_SimpleFilter('url_filter', array($this, 'urlFilter'), array('is_safe' => array('html'), 'needs_environment' => TRUE)), new \Twig_SimpleFilter('placeholder', [$this, 'escapePlaceholder'], array('is_safe' => array('html'), 'needs_environment' => TRUE)), // Replace twig's escape filter with our own. @@ -346,6 +348,21 @@ public function escapePlaceholder($env, $string) { } /** + * Escapes URLs to HTML and strips them of dangerous protocols. + * + * @param \Twig_Environment $env + * A Twig_Environment instance. + * @param mixed $string + * The URL. + * + * @return null|string + * The sanitized, rendered output, or NULL if there is no valid output. + */ + public function urlFilter($env, $string) { + return Html::escape($env, UrlHelper::stripDangerousProtocols($string)); + } + + /** * Overrides twig_escape_filter(). * * Replacement function for Twig's escape filter. diff --git a/core/lib/Drupal/Core/Template/TwigNodeTrans.php b/core/lib/Drupal/Core/Template/TwigNodeTrans.php index a7b723a..4f80ec2 100644 --- a/core/lib/Drupal/Core/Template/TwigNodeTrans.php +++ b/core/lib/Drupal/Core/Template/TwigNodeTrans.php @@ -142,6 +142,9 @@ protected function compileString(\Twig_NodeInterface $body) { case 'placeholder': $argPrefix = '%'; break; + case 'url_filter': + $argPrefix = ':'; + break; } $args = $args->getNode('node'); } diff --git a/core/modules/system/src/Tests/Theme/TwigTransTest.php b/core/modules/system/src/Tests/Theme/TwigTransTest.php index 21aef18..2ddb6aa 100644 --- a/core/modules/system/src/Tests/Theme/TwigTransTest.php +++ b/core/modules/system/src/Tests/Theme/TwigTransTest.php @@ -172,6 +172,12 @@ protected function assertTwigTransTags() { 'O HAI NU TXTZZZZ.', '{% trans with {"context": "Lolspeak", "langcode": "zz"} %} was successfully translated with context in specified language.' ); + + $this->assertText( + "Kittens be full 'o awe: alert(0)", + "{% trans %} Kittens are awesome: {{ 'javascript:alert(0)'|url_filter }} {% endtrans %} was succesfully escaped and translated" + ); + // Makes sure https://www.drupal.org/node/2489024 doesn't happen without // twig debug. $this->assertNoText(pi(), 'Running php code inside a Twig trans is not possible.'); @@ -256,6 +262,9 @@ protected function poFileContents($langcode) { msgid "Pass-through: !string" msgstr "PAS-THRU: !string" +msgid "Kittens are awesome: :url" +msgstr "Kittens be full 'o awe: :url" + msgid "Placeholder: %string" msgstr "PLAYSHOLDR: %string" diff --git a/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans.html.twig b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans.html.twig index 9623ba5..56e9d8f 100644 --- a/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans.html.twig +++ b/core/modules/system/tests/modules/twig_theme_test/templates/twig_theme_test.trans.html.twig @@ -101,3 +101,11 @@ Number I never remember: ' . print(pi()) . ' {% endtrans %} + +{# Tests URL escaping. #} +
+ {% set url = 'javascript:alert(0)' %} + {% trans %} + Kittens are awesome: {{ url|url_filter }} + {% endtrans %} +
diff --git a/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php b/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php index 3e78e91..2376dc7 100644 --- a/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php +++ b/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Render\RenderableInterface; use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Template\Loader\StringLoader; use Drupal\Core\Template\TwigEnvironment; use Drupal\Core\Template\TwigExtension; use Drupal\Tests\UnitTestCase; @@ -129,6 +130,22 @@ public function testSafeStringEscaping() { } /** + * Tests the escaping of objects implementing SafeStringInterface. + * + * @covers ::urlFilter + */ + public function testUrlEscaping() { + $renderer = $this->getMock('\Drupal\Core\Render\RendererInterface'); + $twig_extension = new TwigExtension($renderer); + + $loader = new StringLoader(); + $twig = new \Twig_Environment($loader); + $twig->addExtension($twig_extension); + $result = $twig->render("{{ 'javascript:alert(0)'|url_filter }}"); + $this->assertEquals('alert(0)', $result); + } + + /** * @covers ::safeJoin */ public function testSafeJoin() {