diff --git a/core/lib/Drupal/Component/Utility/Xss.php b/core/lib/Drupal/Component/Utility/Xss.php
index 3cd3a33..2610992 100644
--- a/core/lib/Drupal/Component/Utility/Xss.php
+++ b/core/lib/Drupal/Component/Utility/Xss.php
@@ -192,6 +192,7 @@ protected static function attributes($attributes) {
     $mode = 0;
     $attribute_name = '';
     $skip = FALSE;
+    $harmless = FALSE;
 
     while (strlen($attributes) != 0) {
       // Was the last operation successful?
@@ -203,6 +204,9 @@ protected static function attributes($attributes) {
           if (preg_match('/^([-a-zA-Z]+)/', $attributes, $match)) {
             $attribute_name = strtolower($match[1]);
             $skip = ($attribute_name == 'style' || substr($attribute_name, 0, 2) == 'on');
+            $harmless = substr($attribute_name, 0, 5) === 'data-' || in_array($attribute_name, array(
+              'title',
+            ));
             $working = $mode = 1;
             $attributes = preg_replace('/^[-a-zA-Z]+/', '', $attributes);
           }
@@ -228,7 +232,7 @@ protected static function attributes($attributes) {
         case 2:
           // Attribute value, a URL after href= for instance.
           if (preg_match('/^"([^"]*)"(\s+|$)/', $attributes, $match)) {
-            $thisval = UrlHelper::filterBadProtocol($match[1]);
+            $thisval = $harmless ? $match[1] : UrlHelper::filterBadProtocol($match[1]);
 
             if (!$skip) {
               $attributes_array[] = "$attribute_name=\"$thisval\"";
@@ -240,7 +244,7 @@ protected static function attributes($attributes) {
           }
 
           if (preg_match("/^'([^']*)'(\s+|$)/", $attributes, $match)) {
-            $thisval = UrlHelper::filterBadProtocol($match[1]);
+            $thisval = $harmless ? $match[1] : UrlHelper::filterBadProtocol($match[1]);
 
             if (!$skip) {
               $attributes_array[] = "$attribute_name='$thisval'";
@@ -251,7 +255,7 @@ protected static function attributes($attributes) {
           }
 
           if (preg_match("%^([^\s\"']+)(\s+|$)%", $attributes, $match)) {
-            $thisval = UrlHelper::filterBadProtocol($match[1]);
+            $thisval = $harmless ? $match[1] : UrlHelper::filterBadProtocol($match[1]);
 
             if (!$skip) {
               $attributes_array[] = "$attribute_name=\"$thisval\"";
diff --git a/core/tests/Drupal/Tests/Component/Utility/XssTest.php b/core/tests/Drupal/Tests/Component/Utility/XssTest.php
index 3a090b8..a8c6383 100644
--- a/core/tests/Drupal/Tests/Component/Utility/XssTest.php
+++ b/core/tests/Drupal/Tests/Component/Utility/XssTest.php
@@ -486,6 +486,41 @@ public function testQuestionSign() {
   }
 
   /**
+   * Check that strings in html attributes are are correctly processed.
+   *
+   * @dataProvider providerTestAttributes
+   */
+  public function testAttribute($value, $expected, $message, $allowed_tags = NULL) {
+    if ($allowed_tags === NULL) {
+      $value = Xss::filter($value);
+    }
+    else {
+      $value = Xss::filter($value, $allowed_tags);
+    }
+    $this->assertEquals($expected, $value, $message);
+  }
+
+  /**
+   * Data provider for testFilterXssAdminNotNormalized().
+   */
+  public function providerTestAttributes() {
+    return array(
+      array(
+        '<img src="http://example.com/foo.jpg" title="Example: title" alt="Example: alt">',
+        '<img src="http://example.com/foo.jpg" title="Example: title" alt="Example: alt">',
+        'Image tag with alt and title attribute',
+        array('img')
+      ),
+      array(
+        '<img src="http://example.com/foo.jpg" data-caption="Drupal 8: The best release ever.">',
+        '<img src="http://example.com/foo.jpg" data-caption="Drupal 8: The best release ever.">',
+        'Image tag with alt and title attribute',
+        array('img')
+      ),
+    );
+  }
+
+  /**
    * Checks that \Drupal\Component\Utility\Xss::filterAdmin() correctly strips unallowed tags.
    */
   public function testFilterXSSAdmin() {
