diff --git a/core/lib/Drupal/Component/Utility/AttributeSafeStringInterface.php b/core/lib/Drupal/Component/Utility/AttributeSafeStringInterface.php
new file mode 100644
index 0000000..8a4c4d7
--- /dev/null
+++ b/core/lib/Drupal/Component/Utility/AttributeSafeStringInterface.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Utility\AttributeSafeStringInterface.
+ */
+
+namespace Drupal\Component\Utility;
+
+/**
+ * Marks an object's __toString() method as returning a string safe for an HTML
+ * attribute.
+ *
+ * All objects that implement this interface should be marked @internal.
+ *
+ * This interface should only be used on objects that emit known safe strings
+ * from their __toString() method. If there is any risk of the method returning
+ * user-entered data that has not been escaped first, it must not be used.
+ *
+ * If the object is going to be used directly in Twig templates it should
+ * implement \Countable so it can be used in if statements.
+ *
+ * @internal
+ *   This interface is marked as internal because it should only be used by
+ *   objects used during rendering. This interface should be used by modules if
+ *   they interrupt the render pipeline and explicitly deal with SafeString
+ *   objects created by the render system. Additionally, if a module reuses the
+ *   regular render pipeline internally and passes processed data into it. For
+ *   example, Views implements a custom render pipeline in order to render JSON
+ *   and to fast render fields.
+ */
+interface AttributeSafeStringInterface extends SafeStringInterface {
+
+  /**
+   * Returns a safe string.
+   *
+   * @return string
+   *   The safe string.
+   */
+  public function __toString();
+
+}
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/AttributeSafeString.php b/core/lib/Drupal/Core/Template/AttributeSafeString.php
new file mode 100644
index 0000000..d970385
--- /dev/null
+++ b/core/lib/Drupal/Core/Template/AttributeSafeString.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\SafeString.
+ */
+
+namespace Drupal\Core\Template;
+
+use Drupal\Component\Utility\AttributeSafeStringInterface;
+use Drupal\Component\Utility\SafeStringTrait;
+
+/**
+ * Defines an object that passes safe strings through the render system.
+ *
+ * This object should only be constructed with a known safe string. If there is
+ * any risk that the string contains user-entered data that has not been
+ * escaped first, it must not be used.
+ *
+ * @internal
+ *   This object is marked as internal because it should only be used whilst
+ *   rendering.
+ */
+final class AttributeSafeString implements AttributeSafeStringInterface, \Countable {
+  use SafeStringTrait;
+}
diff --git a/core/lib/Drupal/Core/Template/AttributeString.php b/core/lib/Drupal/Core/Template/AttributeString.php
index 4f131b0..f5bfc91 100644
--- a/core/lib/Drupal/Core/Template/AttributeString.php
+++ b/core/lib/Drupal/Core/Template/AttributeString.php
@@ -8,6 +8,8 @@
 namespace Drupal\Core\Template;
 
 use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Component\Utility\AttributeSafeStringInterface;
 
 /**
  * A class that represents most standard HTML attributes.
@@ -30,7 +32,17 @@ 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 ($this->value instanceof AttributeSafeStringInterface) {
+      return (string) $this->value;
+    }
+    else if (substr($this->name, 0, 5) === 'data-' || in_array($this->name, ['title', 'alt', 'value', 'name', 'property', 'typeof', 'rel', 'about', 'content', 'datatype', 'datatype_callback', 'datetime', 'mailto', 'media', 'sizes'])) {
+      return Html::escape($this->value);
+    }
+    else {
+      return Html::escape(UrlHelper::stripDangerousProtocols($this->value));
+    }
   }
 
 }
diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module
index 29ab66f..45ba795 100644
--- a/core/modules/responsive_image/responsive_image.module
+++ b/core/modules/responsive_image/responsive_image.module
@@ -7,7 +7,9 @@
 
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Routing\RouteMatchInterface;
-use \Drupal\Core\Template\Attribute;
+use Drupal\Core\Template\Attribute;
+use Drupal\Core\Template\AttributeString;
+use Drupal\Core\Template\AttributeSafeString;
 use Drupal\image\Entity\ImageStyle;
 use Drupal\Core\Url;
 use Drupal\responsive_image\Entity\ResponsiveImageStyle;
@@ -392,9 +394,10 @@ function responsive_image_build_source_attributes(ImageInterface $image, array $
   }
   // Sort the srcset from small to large image width or multiplier.
   ksort($srcset);
-  $source_attributes = new \Drupal\Core\Template\Attribute(array(
-    'srcset' => implode(', ', array_unique($srcset)),
-  ));
+  $source_attributes = new \Drupal\Core\Template\Attribute();
+  // The srcset attributes may include data: for 1px gifs so mark it as safe to
+  // avoid the data: protocol being escaped.
+  $source_attributes->setAttribute('srcset', new AttributeString('srcset', AttributeSafeString::create(implode(', ', array_unique($srcset)))));
   $media_query = trim($breakpoint->getMediaQuery());
   if (!empty($media_query)) {
     $source_attributes->setAttribute('media', $media_query);
@@ -467,7 +470,7 @@ function _responsive_image_image_style_url($style_name, $path) {
   if ($style_name == RESPONSIVE_IMAGE_EMPTY_IMAGE) {
     // The smallest data URI for a 1px square transparent GIF image.
     // http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever
-    return 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
+    return AttributeSafeString::create('data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==');
   }
   $entity = ImageStyle::load($style_name);
   if ($entity instanceof Drupal\image\Entity\ImageStyle) {
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..a7a5672 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="alert(String.fromCharCode(88,83,83))"'];
+
+    return $data;
+  }
+
 }
