diff --git a/core/lib/Drupal/Component/Utility/SafeMarkup.php b/core/lib/Drupal/Component/Utility/SafeMarkup.php
index 3020bbe..25bf2d9 100644
--- a/core/lib/Drupal/Component/Utility/SafeMarkup.php
+++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php
@@ -179,13 +179,37 @@ public static function checkPlain($text) {
    *   any key in $args are replaced with the corresponding value, after
    *   optional sanitization and formatting. The type of sanitization and
    *   formatting depends on the first character of the key:
-   *   - @variable: Escaped to HTML using self::escape(). Use this as the
-   *     default choice for anything displayed on a page on the site.
-   *   - %variable: Escaped to HTML wrapped in <em> tags, which makes the
-   *     following HTML code:
+   *   - @variable: Escaped to HTML using Html::escape() unless the value is
+   *     already HTML-safe. Use this as the default choice for anything
+   *     displayed on a page on the site, but not within HTML attributes.
+   *   - %variable: Escaped to HTML just like @variable, but also wrapped in
+   *     <em> tags, which makes the following HTML code:
    *     @code
    *       <em class="placeholder">text output here.</em>
    *     @endcode
+   *     As with @variable, do not use this within HTML attributes.
+   *   - :variable: Use this placeholder if you pass in a URL. The URL is
+   *     sanitized for use in an HTML attribute (such as "src" or "href"). This
+   *     is the only placeholder type that is secure for use within attributes.
+   *     The exact behavior depends on the value:
+   *     - If the value is a well-formed (per RFC 3986) relative URL or
+   *       absolute URL that does not use a dangerous protocol (like
+   *       "javascript:"), then the URL is escaped to HTML. This includes all
+   *       URLs generated via \Drupal\Core\Url::toString() and
+   *       \Drupal\Core\Routing\UrlGeneratorTrait::url().
+   *     - If the value is a well-formed absolute URL with a dangerous
+   *       protocol, the protocol is stripped. This process is repeated on the
+   *       remaining URL until it is stripped down to a safe protocol. Then the
+   *       remaining URL is escaped to HTML.
+   *     - If the value is not a well-formed URL, the sanitization behavior is
+   *       undefined. Currently, it invokes the same logic as for well-formed
+   *       URLs, which strips most substrings that precede a ":" and escapes
+   *       for HTML, but that may change at any time, including in a patch
+   *       release of Drupal. The result is secure to use within attributes,
+   *       but may not produce valid HTML (e.g., malformed URLs within "href"
+   *       attributes fail HTML validation). This can be avoided by using
+   *       \Drupal\Core\Url::fromUri($possibly_not_a_url)->toString(), which
+   *       either throws an exception or returns a well-formed URL.
    *   - !variable: Inserted as is, with no sanitization or formatting. Only
    *     use this when the resulting string is being generated for one of:
    *     - Non-HTML usage, such as a plain-text email.
@@ -201,6 +225,7 @@ public static function checkPlain($text) {
    * @ingroup sanitization
    *
    * @see t()
+   * @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols()
    */
   public static function format($string, array $args) {
     $safe = TRUE;
@@ -215,6 +240,10 @@ public static function format($string, array $args) {
           }
           break;
 
+        case ':':
+          $args[$key] = Html::escape(UrlHelper::stripDangerousProtocols($value));
+          break;
+
         case '%':
         default:
           // Escaped and placeholder.
diff --git a/core/tests/Drupal/KernelTests/Component/Utility/SafeMarkupKernelTest.php b/core/tests/Drupal/KernelTests/Component/Utility/SafeMarkupKernelTest.php
new file mode 100644
index 0000000..5bdb3cc
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Component/Utility/SafeMarkupKernelTest.php
@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\KernelTests\Component\Utility\SafeMarkupKernelTest.
+ */
+
+namespace Drupal\KernelTests\Component\Utility;
+
+use Drupal\Component\FileCache\FileCacheFactory;
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Site\Settings;
+use Drupal\Core\Url;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Provides a test covering integration of SafeMarkup with other systems.
+ *
+ * @group Utility
+*/
+class SafeMarkupKernelTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['system'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    // @todo Extra hack to avoid test fails, remove this once
+    //   https://www.drupal.org/node/2553661 is fixed.
+    FileCacheFactory::setPrefix(Settings::getApcuPrefix('file_cache', $this->root));
+
+    parent::setUp();
+
+    $this->installSchema('system', 'router');
+    $this->container->get('router.builder')->rebuild();
+  }
+
+  /**
+   * Utility function to prepare args for SafeMarkup.
+   *
+   * @param array $args
+   *   Array containing:
+   *   - ':url': Arguments to pass to Url::fromUri().
+   *
+   * @return array
+   *   Array containing:
+   *   - ':url': A URL string.
+   */
+  protected static function prepareSafeMarkupArgs($args) {
+    $args[':url'] = call_user_func_array([Url::class, 'fromUri'], $args[':url'])->toString();
+    return $args;
+  }
+
+  /**
+   * @dataProvider providerTestSafeMarkup
+   */
+  public function testSafeMarkup($string, array $args, $expected) {
+    $args = self::prepareSafeMarkupArgs($args);
+    $this->assertEquals($expected, SafeMarkup::format($string, $args));
+  }
+
+  /**
+   * @return array
+   */
+  public function providerTestSafeMarkup() {
+    $data = [];
+    $data['routed-url'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => ['route:system.admin']], 'Hey giraffe <a href="/admin">MUUUH</a>'];
+    $data['routed-with-query'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => ['route:system.admin', ['query' => ['bar' => 'baz#']]]], 'Hey giraffe <a href="/admin?bar=baz%23">MUUUH</a>'];
+    $data['routed-with-fragment'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => ['route:system.admin', ['fragment' => 'bar&lt;']]], 'Hey giraffe <a href="/admin#bar&amp;lt;">MUUUH</a>'];
+    $data['unrouted-url'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => ['base://foo']], 'Hey giraffe <a href="/foo">MUUUH</a>'];
+    $data['unrouted-with-query'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => ['base://foo', ['query' => ['bar' => 'baz#']]]], 'Hey giraffe <a href="/foo?bar=baz%23">MUUUH</a>'];
+    $data['unrouted-with-fragment'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => ['base://foo', ['fragment' => 'bar&lt;']]], 'Hey giraffe <a href="/foo#bar&amp;lt;">MUUUH</a>'];
+    $data['mailto-protocol'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => ['mailto:test@example.com']], 'Hey giraffe <a href="mailto:test@example.com">MUUUH</a>'];
+
+    return $data;
+  }
+
+  /**
+   * @dataProvider providerTestSafeMarkupWithException
+   * @expectedException \InvalidArgumentException
+   */
+  public function testSafeMarkupWithException($string, array $args) {
+    // Should throw an \InvalidArgumentException, due to Uri::toString().
+    $args = self::prepareSafeMarkupArgs($args);
+
+    SafeMarkup::format($string, $args);
+  }
+
+  /**
+   * @return array
+   */
+  public function providerTestSafeMarkupWithException() {
+    $data = [];
+    $data['js-protocol'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => ["javascript:alert('xss')"]]];
+    $data['js-with-fromCharCode'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => ["javascript:alert(String.fromCharCode(88,83,83))"]]];
+    $data['non-url-with-colon'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => ["llamas: they are not URLs"]]];
+    $data['non-url-with-html'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => ['<span>not a url</span>']]];
+
+    return $data;
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/DrupalComponentTest.php b/core/tests/Drupal/Tests/Component/DrupalComponentTest.php
index f574106..0a23f3a 100644
--- a/core/tests/Drupal/Tests/Component/DrupalComponentTest.php
+++ b/core/tests/Drupal/Tests/Component/DrupalComponentTest.php
@@ -68,6 +68,13 @@ protected function findPhpClasses($dir) {
    */
   protected function assertNoCoreUsage($class_path) {
     $contents = file_get_contents($class_path);
+
+    // Remove multiline comments.
+    $contents = preg_replace('@/\*.*?\*/@s', '', $contents);
+    $contents = preg_replace('/\n\s*\n/', "\n", $contents);
+    // Removes single line '//' comments.
+    $contents = preg_replace('@[ \t]*//.*[ \t]*[\r\n]@', '', $contents);
+
     preg_match_all('/^.*Drupal\\\Core.*$/m', $contents, $matches);
     $matches = array_filter($matches[0], function($line) {
       // Filter references to @see as they don't really matter.
diff --git a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
index fae15bb..12f2012 100644
--- a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
+++ b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
@@ -10,6 +10,7 @@
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\SafeStringInterface;
 use Drupal\Component\Utility\SafeStringTrait;
+use Drupal\Component\Utility\UrlHelper;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -21,6 +22,16 @@
 class SafeMarkupTest extends UnitTestCase {
 
   /**
+   * {@inheritdoc}
+   */
+  protected function tearDown() {
+    parent::tearDown();
+
+    UrlHelper::setAllowedProtocols(['http', 'https']);
+  }
+
+
+  /**
    * Helper function to add a string to the safe list for testing.
    *
    * @param string $string
@@ -198,6 +209,8 @@ function providerCheckPlain() {
    *   Whether the result is expected to be safe for HTML display.
    */
   public function testFormat($string, array $args, $expected, $message, $expected_is_safe) {
+    UrlHelper::setAllowedProtocols(['http', 'https', 'mailto']);
+
     $result = SafeMarkup::format($string, $args);
     $this->assertEquals($expected, $result, $message);
     $this->assertEquals($expected_is_safe, SafeMarkup::isSafe($result), 'SafeMarkup::format correctly sets the result as safe or not safe.');
@@ -221,6 +234,20 @@ function providerFormat() {
     $tests[] = array('Verbatim text: !value', array('!value' => '<script>'), 'Verbatim text: <script>', 'SafeMarkup::format replaces verbatim string as-is.', FALSE);
     $tests[] = array('Verbatim text: !value', array('!value' => SafeMarkupTestSafeString::create('<span>Safe HTML</span>')), 'Verbatim text: <span>Safe HTML</span>', 'SafeMarkup::format replaces verbatim string as-is.', TRUE);
 
+    $tests['javascript-protocol-url'] = ['Simple text <a href=":url">giraffe</a>', [':url' => 'javascript://example.com?foo&bar'], 'Simple text <a href="//example.com?foo&amp;bar">giraffe</a>', 'Support for filtering bad protocols', TRUE];
+    $tests['external-url'] = ['Simple text <a href=":url">giraffe</a>', [':url' => 'http://example.com?foo&bar'], 'Simple text <a href="http://example.com?foo&amp;bar">giraffe</a>', 'Support for filtering bad protocols', TRUE];
+    $tests['relative-url'] = ['Simple text <a href=":url">giraffe</a>', [':url' => '/node/1?foo&bar'], 'Simple text <a href="/node/1?foo&amp;bar">giraffe</a>', 'Support for filtering bad protocols', TRUE];
+    $tests['fragment-with-special-chars'] = ['Simple text <a href=":url">giraffe</a>', [':url' => 'http://example.com/#&lt;'], 'Simple text <a href="http://example.com/#&amp;lt;">giraffe</a>', 'Support for filtering bad protocols', TRUE];
+    $tests['mailto-protocol'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => 'mailto:test@example.com'], 'Hey giraffe <a href="mailto:test@example.com">MUUUH</a>', '', TRUE];
+    $tests['js-with-fromCharCode'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => "javascript:alert(String.fromCharCode(88,83,83))"], 'Hey giraffe <a href="alert(String.fromCharCode(88,83,83))">MUUUH</a>', '', TRUE];
+
+    // Test some "URL" values that are not RFC 3986 compliant URLs. The result
+    // of SafeMarkup::format() should still be valid HTML (other than the
+    // value of the "href" attribute not being a valid URL), and not
+    // vulnerable to XSS.
+    $tests['non-url-with-colon'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => "llamas: they are not URLs"], 'Hey giraffe <a href=" they are not URLs">MUUUH</a>', '', TRUE];
+    $tests['non-url-with-html'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => "<span>not a url</span>"], 'Hey giraffe <a href="&lt;span&gt;not a url&lt;/span&gt;">MUUUH</a>', '', TRUE];
+
     return $tests;
   }
 
