diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 48b3bcd..5bb4916 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -705,15 +705,14 @@ protected function createPlaceholder(array $element) {
     // is unique markup that isn't easily guessable. The #lazy_builder callback
     // and its arguments are put in the placeholder markup solely to simplify
     // debugging.
-    $attributes = new Attribute();
-    $attributes['callback'] = $placeholder_render_array['#lazy_builder'][0];
-    $attributes['arguments'] = UrlHelper::buildQuery($placeholder_render_array['#lazy_builder'][1]);
-    $attributes['token'] = hash('crc32b', serialize($placeholder_render_array));
-    $placeholder_markup = SafeMarkup::format('<drupal-render-placeholder@attributes></drupal-render-placeholder>', ['@attributes' => $attributes]);
+    $callback = $placeholder_render_array['#lazy_builder'][0];
+    $arguments = UrlHelper::buildQuery($placeholder_render_array['#lazy_builder'][1]);
+    $token = hash('crc32b', serialize($placeholder_render_array));
+    $placeholder_markup = '<drupal-render-placeholder callback="' . $callback . '" arguments="' . $arguments . '" token="' . $token . '"></drupal-render-placeholder>';
 
     // Build the placeholder element to return.
     $placeholder_element = [];
-    $placeholder_element['#markup'] = $placeholder_markup;
+    $placeholder_element['#markup'] = SafeString::create($placeholder_markup);
     $placeholder_element['#attached']['placeholders'][$placeholder_markup] = $placeholder_render_array;
     return $placeholder_element;
   }
diff --git a/core/lib/Drupal/Core/Template/AttributeString.php b/core/lib/Drupal/Core/Template/AttributeString.php
index 4f131b0..161f811 100644
--- a/core/lib/Drupal/Core/Template/AttributeString.php
+++ b/core/lib/Drupal/Core/Template/AttributeString.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core\Template;
 
 use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\UrlHelper;
 
 /**
  * A class that represents most standard HTML attributes.
@@ -30,7 +31,14 @@ class AttributeString extends AttributeValueBase {
    * Implements the magic __toString() method.
    */
   public function __toString() {
-    return Html::escape($this->value);
+    // Whitelist 'title', 'alt', and all data- attributes.
+    // @see Xss::attributes()
+    if (substr($this->name, 0, 5) === 'data-' || in_array($this->name, ['title', 'alt', 'value', 'name', 'property', 'typeof', 'rel', 'about', 'content', 'datatype', 'datatype_callback', 'datetime'])) {
+      return Html::escape($this->value);
+    }
+    else {
+      return Html::escape(UrlHelper::stripDangerousProtocols($this->value));
+    }
   }
 
 }
diff --git a/core/tests/Drupal/Tests/Core/Template/AttributeTest.php b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php
index b4a192d..1f42ab7 100644
--- a/core/tests/Drupal/Tests/Core/Template/AttributeTest.php
+++ b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\Core\Template;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Template\AttributeArray;
 use Drupal\Core\Template\AttributeString;
@@ -420,4 +421,41 @@ public function testStorage() {
     $this->assertEquals(array('class' => new AttributeArray('class', array('example-class'))), $attribute->storage());
   }
 
+  /**
+   * @dataProvider providerTestAttributesWithUrls
+   */
+  public function testAttributesWithUrls($attributes, $expected_output) {
+    $attribute = new Attribute($attributes);
+
+    $this->assertEquals($expected_output, $attribute->__toString());
+  }
+
+  public function providerTestAttributesWithUrls() {
+    $data = [];
+    $data['normal-external-url'] = [['href' => "http://example.com/foo"], ' href="http://example.com/foo"'];
+    $data['url-with-query-string-fragment'] = [['href' => "http://example.com/com?foo=bar#baz;&lt"], ' href="http://example.com/com?foo=bar#baz;&amp;lt"'];
+
+    $data['data-value'] = [['data-example' => 'http://example.com/foo'], ' data-example="http://example.com/foo"'];
+    $data['data-xss-value'] = [['data-example' => "javascript:alert('xss');"], ' data-example="' . Html::escape("javascript:alert('xss');") . '"'];
+
+    $escaped = Html::escape("alert('xss');");
+    $data['xss-scheme-href'] = [['href' => "javascript:alert('xss');"], ' href="' . $escaped . '"'];
+    $data['xss-scheme-src'] = [['src' => "javascript:alert('xss');"], ' src="' . $escaped . '"'];
+    $data['xss-scheme-dynsrc'] = [['dynsrc' => "javascript:alert('xss');"], ' dynsrc="' . $escaped . '"'];
+    $data['xss-scheme-background'] = [['background' => "javascript:alert('xss');"], ' background="' . $escaped . '"'];
+    $data['xss-scheme-src-case'] = [['src' => "jaVaSCriPt:alert('xss');"], ' src="' . $escaped . '"'];
+
+    $already_escaped = '&lt;script defer&gt;alert(0)&lt;/script&gt;';
+    $data['xss-already-escaped'] = [['href' => $already_escaped], ' href="' . Html::escape($already_escaped) . '"'];
+
+    $data['xss-in-style'] = [['style' => 'list-style-image: url(javascript:alert(0))'], ' style="alert(0))"'];
+
+    // Ensure mailto: protocol passes through.
+    $data['mailto'] = [['href' => 'mailto:me@example.com'], ' href="mailto:me@example.com"'];
+    // Ensure people don't try to work around escaping quotes.
+    $data['mailto'] = [['href' => 'javascript:alert(String.fromCharCode(88,83,83))'], ' href=""'];
+
+    return $data;
+  }
+
 }
