diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 48b3bcd..9927268 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -706,7 +706,7 @@ protected function createPlaceholder(array $element) {
     // 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['data-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]);
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/Render/RendererPlaceholdersTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
index bf8406d..3fea140 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
@@ -72,7 +72,7 @@ public function providerPlaceholders() {
       // \Drupal\Core\Render\SafeString::create() is necessary as the render
       // system would mangle this markup. As this is exactly what happens at
       // runtime this is a valid use-case.
-      return SafeString::create('<drupal-render-placeholder callback="Drupal\Tests\Core\Render\PlaceholdersTest::callback" arguments="' . '0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>');
+      return SafeString::create('<drupal-render-placeholder data-callback="Drupal\Tests\Core\Render\PlaceholdersTest::callback" arguments="' . '0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>');
     };
 
     $extract_placeholder_render_array = function ($placeholder_render_array) {
@@ -441,7 +441,7 @@ public function testCacheableParent($test_element, $args, array $expected_placeh
     $this->setUpRequest('GET');
 
     $token = hash('crc32b', serialize($expected_placeholder_render_array));
-    $expected_placeholder_markup = '<drupal-render-placeholder callback="Drupal\Tests\Core\Render\PlaceholdersTest::callback" arguments="0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>';
+    $expected_placeholder_markup = '<drupal-render-placeholder data-callback="Drupal\Tests\Core\Render\PlaceholdersTest::callback" arguments="0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>';
     $this->assertSame($expected_placeholder_markup, Html::normalize($expected_placeholder_markup), 'Placeholder unaltered by Html::normalize() which is used by FilterHtmlCorrector.');
 
     // GET request: #cache enabled, cache miss.
diff --git a/core/tests/Drupal/Tests/Core/Template/AttributeTest.php b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php
index b4a192d..1f3203d 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,36 @@ 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))"'];
+
+    return $data;
+  }
+
 }
