diff --git a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
index 2e469da..7fbdb22 100644
--- a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
+++ b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
@@ -496,6 +496,13 @@ protected function generateACFSettings(Editor $editor) {
         }
         // Tell CKEditor the tag is allowed, along with some tags.
         elseif (is_array($attributes)) {
+          // Set defaults (these will be overridden below if more specific
+          // values are present).
+          $allowed[$tag] = array(
+            'attributes' => FALSE,
+            'styles' => FALSE,
+            'classes' => FALSE,
+          );
           // Configure allowed attributes, allowed "style" attribute values and
           // allowed "class" attribute values.
           // CKEditor only allows specific values for the "class" and "style"
@@ -560,6 +567,9 @@ protected function generateACFSettings(Editor $editor) {
         }
       }
 
+      ksort($allowed);
+      ksort($disallowed);
+
       return array($allowed, $disallowed);
     }
   }
diff --git a/core/modules/ckeditor/src/Tests/CKEditorTest.php b/core/modules/ckeditor/src/Tests/CKEditorTest.php
index d2a59bc..6e65ce2 100644
--- a/core/modules/ckeditor/src/Tests/CKEditorTest.php
+++ b/core/modules/ckeditor/src/Tests/CKEditorTest.php
@@ -54,7 +54,7 @@ protected function setUp() {
         'filter_html' => array(
           'status' => 1,
           'settings' => array(
-            'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a>',
+            'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a href hreflang>',
           )
         ),
       ),
@@ -95,6 +95,7 @@ function testGetJSSettings() {
       ),
     );
     ksort($expected_config);
+    ksort($expected_config['allowedContent']);
     $this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for default configuration.');
 
     // Customize the configuration: add button, have two contextually enabled
@@ -121,12 +122,13 @@ function testGetJSSettings() {
     // Change the allowed HTML tags; the "allowedContent" and "format_tags"
     // settings for CKEditor should automatically be updated as well.
     $format = $editor->getFilterFormat();
-    $format->filters('filter_html')->settings['allowed_html'] .= '<pre> <h3>';
+    $format->filters('filter_html')->settings['allowed_html'] .= '<pre> <h1>';
     $format->save();
 
-    $expected_config['allowedContent']['pre'] = array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE);
-    $expected_config['allowedContent']['h3'] = array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE);
-    $expected_config['format_tags'] = 'p;h2;h3;h4;h5;h6;pre';
+    $expected_config['allowedContent']['pre'] = array('attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE);
+    $expected_config['allowedContent']['h1'] = array('attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE);
+    $expected_config['format_tags'] = 'p;h1;h2;h3;h4;h5;h6;pre';
+    ksort($expected_config['allowedContent']);
     $this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
 
     // Disable the filter_html filter: allow *all *tags.
@@ -178,14 +180,17 @@ function testGetJSSettings() {
       ),
       'a' => array(
         'attributes' => 'href,rel,class,target',
+        'styles' => FALSE,
         'classes' => 'external',
       ),
       'span' => array(
         'attributes' => 'class,property,rel,style',
         'styles' => 'font-size',
+        'classes' => FALSE,
       ),
       '*' => array(
         'attributes' => 'class,data-*',
+        'styles' => FALSE,
         'classes' => 'is-a-hipster-llama,and-more',
       ),
       'del' => array(
@@ -205,6 +210,8 @@ function testGetJSSettings() {
     );
     $expected_config['format_tags'] = 'p';
     ksort($expected_config);
+    ksort($expected_config['allowedContent']);
+    ksort($expected_config['disallowedContent']);
     $this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
   }
 
@@ -420,17 +427,18 @@ protected function getDefaultInternalConfig() {
   }
 
   protected function getDefaultAllowedContentConfig() {
-    return array(
-      'h2' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
-      'h3' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
-      'h4' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
-      'h5' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
-      'h6' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
-      'p' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
-      'br' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
-      'strong' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
-      'a' => array('attributes' => TRUE, 'styles' => FALSE, 'classes' => TRUE),
-    );
+    return [
+      'h2' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
+      'h3' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
+      'h4' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
+      'h5' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
+      'h6' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
+      'p' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
+      'br' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
+      'strong' => ['attributes' => FALSE, 'styles' => FALSE, 'classes' => FALSE],
+      'a' => ['attributes' => 'href,hreflang', 'styles' => FALSE, 'classes' => FALSE],
+      '*' => ['attributes' => 'id,lang,dir', 'styles' => FALSE, 'classes' => FALSE],
+    ];
   }
 
   protected function getDefaultDisallowedContentConfig() {
diff --git a/core/modules/filter/filter.filter_html.admin.js b/core/modules/filter/filter.filter_html.admin.js
index 75a9824..8f94d44 100644
--- a/core/modules/filter/filter.filter_html.admin.js
+++ b/core/modules/filter/filter.filter_html.admin.js
@@ -23,23 +23,15 @@
        */
       getRules: function () {
         var currentValue = $('#edit-filters-filter-html-settings-allowed-html').val();
-        var rules = [];
-        var rule;
+        var rules = Drupal.behaviors.filterFilterHtmlUpdating._parseSetting(currentValue);
 
         // Build a FilterHTMLRule that reflects the hard-coded behavior that
         // strips all "style" attribute and all "on*" attributes.
-        rule = new Drupal.FilterHTMLRule();
+        var rule = new Drupal.FilterHTMLRule();
         rule.restrictedTags.tags = ['*'];
         rule.restrictedTags.forbidden.attributes = ['style', 'on*'];
         rules.push(rule);
 
-        // Build a FilterHTMLRule that reflects the current settings.
-        rule = new Drupal.FilterHTMLRule();
-        var behavior = Drupal.behaviors.filterFilterHtmlUpdating;
-        rule.allow = true;
-        rule.tags = behavior._parseSetting(currentValue);
-        rules.push(rule);
-
         return rules;
       }
     };
@@ -63,8 +55,12 @@
     // The description for the "Allowed HTML tags" field.
     $allowedHTMLDescription: null,
 
-    // The user-entered tag list of $allowedHTMLFormItem.
-    userTags: null,
+    /**
+     * The parsed, user-entered tag list of $allowedHTMLFormItem
+     *
+     * @var {Object.<string, Drupal.FilterHTMLRule>}
+     */
+    userTags: {},
 
     // The auto-created tag list thus far added.
     autoTags: null,
@@ -116,9 +112,10 @@
       this.$allowedHTMLDescription.find('.editor-update-message').remove();
 
       // If any auto-created tags: insert message and update form item.
-      if (this.autoTags.length > 0) {
+      if (!_.isEmpty(this.autoTags)) {
         this.$allowedHTMLDescription.append(Drupal.theme('filterFilterHTMLUpdateMessage', this.autoTags));
-        this.$allowedHTMLFormItem.val(this._generateSetting(this.userTags) + ' ' + this._generateSetting(this.autoTags));
+        var userTagsWithoutOverrides = _.omit(this.userTags, _.keys(this.autoTags));
+        this.$allowedHTMLFormItem.val(this._generateSetting(userTagsWithoutOverrides) + ' ' + this._generateSetting(this.autoTags));
       }
       // Restore to original state.
       else {
@@ -142,45 +139,136 @@
      *   A list of new allowed tags.
      */
     _calculateAutoAllowedTags: function (userAllowedTags, newFeatures) {
-      return _
-        .chain(newFeatures)
-        // Reduce multiple features' rules.
-        .reduce(function (memo, featureRules) {
-          // Reduce a single features' rules' required tags.
-          return _.union(memo, _.reduce(featureRules, function (memo, featureRule) {
-            return _.union(memo, featureRule.required.tags);
-          }, []));
-        }, [])
-        // All new features' required tags are "new allowed tags", except
-        // for those that are already allowed in the original allowed tags.
-        .difference(userAllowedTags)
-        .value();
+      var editorRequiredTags = {};
+      // Map the newly added Text Editor features to Drupal.FilterHtmlRule
+      // objects.
+      // (to allow comparing userTags with autoTags.)
+      var featureName, feature, featureRule, filterRule, tag;
+      for (featureName in newFeatures) {
+        feature = newFeatures[featureName];
+        for (var f = 0; f < feature.length; f++) {
+          featureRule = feature[f];
+          for (var t = 0; t < featureRule.required.tags.length; t++) {
+            tag = featureRule.required.tags[t];
+            if (!_.has(editorRequiredTags, tag)) {
+              filterRule = new Drupal.FilterHTMLRule();
+              filterRule.restrictedTags.tags = [tag];
+              // @todo Neither Drupal.FilterHtmlRule nor
+              //   Drupal.EditorFeatureHTMLRule allow for generic attribute
+              //   value restrictions, only for the "class" and "style"
+              //   attribute's values to be restricted.
+              filterRule.restrictedTags.allowed.attributes = featureRule.required.attributes;
+              editorRequiredTags[tag] = filterRule;
+            }
+            // The tag is already allowed, add any additionally allowed
+            // attributes.
+            else {
+              filterRule = editorRequiredTags[tag];
+              filterRule.restrictedTags.allowed.attributes = _.union(filterRule.restrictedTags.allowed.attributes, featureRule.required.attributes);
+            }
+          }
+        }
+      }
+
+      // Now compare userAllowedTags with editorRequiredTags, and build
+      // autoAllowedTags, which contains:
+      // - any tags in editorRequiredTags but not in userAllowedTags (i.e. tags
+      //   that are additionally going to be allowed)
+      // - any tags in editorRequiredTags that already exists in userAllowedTags
+      //   but does not allow all attributes or attribute values
+      var autoAllowedTags = {};
+      for (tag in editorRequiredTags) {
+        // If userAllowedTags does not contain a rule for this editor-required
+        // tag, then add it to the list of automatically allowed tags.
+        if (!_.has(userAllowedTags, tag)) {
+          autoAllowedTags[tag] = editorRequiredTags[tag];
+        }
+        // Otherwise, if userAllowedTags already allows this tag, then check if
+        // additional attributes on this tag are required by the editor.
+        else {
+          var requiredAttributes = editorRequiredTags[tag].restrictedTags.allowed.attributes;
+          var allowedAttributes = userAllowedTags[tag].restrictedTags.allowed.attributes;
+          if (requiredAttributes.length &&_.difference(requiredAttributes, allowedAttributes).length) {
+            autoAllowedTags[tag] = userAllowedTags[tag];
+            autoAllowedTags[tag].restrictedTags.allowed.attributes = _.union(allowedAttributes, requiredAttributes);
+          }
+          // @todo attribute values.
+        }
+      }
+
+      return autoAllowedTags;
     },
 
     /**
      * Parses the value of this.$allowedHTMLFormItem.
      *
      * @param {string} setting
-     *   The string representation of the setting. e.g. "<p> <br> <a>"
+     *   The string representation of the setting. For example:
+     *     <p class="callout"> <br> <a href hreflang>
      *
-     * @return {Array}
-     *   The array representation of the setting. e.g. ['p', 'br', 'a']
+     * @return {Object.<string, Drupal.FilterHTMLRule>}
+     *   The corresponding text filter HTML rule objects, one per tag, keyed by
+     *   tag name.
      */
     _parseSetting: function (setting) {
-      return setting.length ? setting.substring(1, setting.length - 1).split('> <') : [];
+      var allowedTags = setting.match(/(<[^>]+>)/g);
+      var sandbox = document.createElement('div');
+      var node, tag, rule, attributes, attribute;
+      var rules = {};
+      for (var t = 0; t < allowedTags.length; t++) {
+        // Let the browser do the parsing work for us.
+        sandbox.innerHTML = allowedTags[t];
+        node = sandbox.firstChild;
+        tag = node.tagName.toLowerCase();
+
+        // Build the Drupal.FilterHtmlRule object.
+        rule = new Drupal.FilterHTMLRule();
+        // We create one rule per allowed tag, so always one tag.
+        rule.restrictedTags.tags = [tag];
+        // Add the attribute restrictions.
+        attributes = node.attributes;
+        for (var i = 0; i < attributes.length; i++) {
+          attribute = attributes.item(i);
+          var attributeName = attribute.nodeName;
+          // @todo Drupal.FilterHtmlRule does not allow for generic attribute
+          //   value restrictions, only for the "class" and "style" attribute's
+          //   values.
+          var attributeValue = attribute.textContent;
+          rule.restrictedTags.allowed.attributes.push(attributeName);
+        }
+
+        rules[tag] = rule;
+      }
+      return rules;
     },
 
     /**
      * Generates the value of this.$allowedHTMLFormItem.
      *
-     * @param {Array} tags
-     *   The array representation of the setting. e.g. ['p', 'br', 'a']
+     * @param {Object.<string, Drupal.FilterHTMLRule>} tags
+     *   The parsed representation of the setting.
      *
      * @return {Array}
      *   The string representation of the setting. e.g. "<p> <br> <a>"
      */
     _generateSetting: function (tags) {
-      return tags.length ? '<' + tags.join('> <') + '>' : '';
+      return _.reduce(tags, function (setting, rule, tag) {
+        if (setting.length) {
+          setting += ' ';
+        }
+
+        setting += '<' + tag;
+        if (rule.restrictedTags.allowed.attributes.length) {
+          setting += ' ' + rule.restrictedTags.allowed.attributes.join(' ');
+        }
+        // @todo Drupal.FilterHtmlRule does not allow for generic attribute
+        //   value restrictions, only for the "class" and "style" attribute's
+        //   values. Until generic support is here, we don't bother generating
+        //   attribute value whitelists at all, and we pretend every value is
+        //   allowed for any given attribute.
+        setting += '>';
+        return setting;
+      }, '');
     }
 
   };
@@ -196,7 +284,7 @@
    */
   Drupal.theme.filterFilterHTMLUpdateMessage = function (tags) {
     var html = '';
-    var tagList = '<' + tags.join('> <') + '>';
+    var tagList = Drupal.behaviors.filterFilterHtmlUpdating._generateSetting(tags);
     html += '<p class="editor-update-message">';
     html += Drupal.t('Based on the text editor configuration, these tags have automatically been added: <strong>@tag-list</strong>.', {'@tag-list': tagList});
     html += '</p>';
diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index 6188006..7202b28 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -13,7 +13,7 @@
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Template\Attribute;
-use Drupal\filter\Entity\FilterFormat;
+use Drupal\filter\Plugin\FilterBase;
 use Drupal\filter\FilterFormatInterface;
 
 /**
@@ -453,18 +453,74 @@ function template_preprocess_filter_tips(&$variables) {
 /**
  * Provides filtering of input into accepted HTML.
  */
-function _filter_html($text, $filter) {
-  $allowed_tags = preg_split('/\s+|<|>/', $filter->settings['allowed_html'], -1, PREG_SPLIT_NO_EMPTY);
+function _filter_html($text, FilterBase $filter) {
+  $restrictions = $filter->getHtmlRestrictions();
+  $global_allowed_attrs = array_filter($restrictions['allowed']['*']);
+  unset($restrictions['allowed']['*']);
+  $allowed_tags = array_keys($restrictions['allowed']);
   $text = Xss::filter($text, $allowed_tags);
 
+  // Apply attribute restrictions to tags.
+  $html_dom = Html::load($text);
+  $xpath = new \DOMXPath($html_dom);
+  foreach ($allowed_tags as $allowed_tag) {
+    // By default, no attributes are allowed for a tag, but due to the globally
+    // whitelisted attributes, it is impossible for a tag to actually completely
+    // disallow attributes.
+    if ($restrictions['allowed'][$allowed_tag] === FALSE) {
+      $restrictions['allowed'][$allowed_tag] = $global_allowed_attrs;
+    }
+    // If specific attributes are whitelisted, then ensure our global whitelist
+    // is also applied.
+    elseif (is_array($restrictions['allowed'][$allowed_tag])) {
+      // More specific restrictions on the 'id', 'lang' or 'dir' attributes are
+      // not allowed.
+      $restrictions['allowed'][$allowed_tag] = $global_allowed_attrs + $restrictions['allowed'][$allowed_tag];
+    }
+
+    // First, remove all attributes except the whitelisted ones.
+    $attribute_whitelist = array_keys(array_merge($global_allowed_attrs, $restrictions['allowed'][$allowed_tag]));
+    foreach ($xpath->query('//' . $allowed_tag) as $node) {
+      if ($node->hasAttributes()) {
+        $attribute_list = array_keys(iterator_to_array($node->attributes));
+        $attributes_to_remove = array_diff($attribute_list, $attribute_whitelist);
+        foreach ($attributes_to_remove as $attribute) {
+          $node->removeAttribute($attribute);
+        }
+      }
+    }
+    // Second, remove all attributes whose values do not meet the attribute
+    // values whitelist.
+    foreach ($restrictions['allowed'][$allowed_tag] as $attribute => $allowed_attribute_values) {
+      // All attribute values are allowed, we have no work to do.
+      if ($allowed_attribute_values === TRUE) {
+        continue;
+      }
+      foreach ($xpath->query('//' . $allowed_tag . '[@' . $attribute . ']') as $node) {
+        $attribute_values = preg_split('/\s+/', $node->getAttribute($attribute), -1, PREG_SPLIT_NO_EMPTY);
+        $final_values = [];
+        foreach ($attribute_values as $idx => $value) {
+          if (in_array($value, $allowed_attribute_values)) {
+            $final_values[] = $value;
+          }
+        }
+        if ($final_values) {
+          $node->setAttribute($attribute, implode(' ', $final_values));
+        }
+        else {
+          $node->removeAttribute($attribute);
+        }
+      }
+    }
+  }
+
   if ($filter->settings['filter_html_nofollow']) {
-    $html_dom = Html::load($text);
     $links = $html_dom->getElementsByTagName('a');
     foreach ($links as $link) {
       $link->setAttribute('rel', 'nofollow');
     }
-    $text = Html::serialize($html_dom);
   }
+  $text = Html::serialize($html_dom);
 
   return trim($text);
 }
diff --git a/core/modules/filter/migration_templates/d6_filter_format.yml b/core/modules/filter/migration_templates/d6_filter_format.yml
index 16982d3..c3f896f 100644
--- a/core/modules/filter/migration_templates/d6_filter_format.yml
+++ b/core/modules/filter/migration_templates/d6_filter_format.yml
@@ -36,7 +36,9 @@ process:
             - filter_html_escape
           php:
             - php_code
-      settings: settings
+      settings:
+        plugin: filter_settings
+        source: settings
       status:
         plugin: default_value
         default_value: true
diff --git a/core/modules/filter/src/Plugin/Filter/FilterHtml.php b/core/modules/filter/src/Plugin/Filter/FilterHtml.php
index 4708cc6..f5791b5 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterHtml.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterHtml.php
@@ -20,7 +20,7 @@
  *   title = @Translation("Limit allowed HTML tags"),
  *   type = Drupal\filter\Plugin\FilterInterface::TYPE_HTML_RESTRICTOR,
  *   settings = {
- *     "allowed_html" = "<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <h2> <h3> <h4> <h5> <h6>",
+ *     "allowed_html" = "<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul> <ol start type='1 A I'> <li> <dl> <dt> <dd> <h2> <h3> <h4> <h5> <h6>",
  *     "filter_html_help" = TRUE,
  *     "filter_html_nofollow" = FALSE
  *   },
@@ -39,6 +39,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) {
       '#default_value' => $this->settings['allowed_html'],
       '#maxlength' => 1024,
       '#description' => $this->t('A list of HTML tags that can be used. JavaScript event attributes, JavaScript URLs, and CSS are always stripped.'),
+      '#size' => 250,
       '#attached' => array(
         'library' => array(
           'filter/drupal.filter.filter_html.admin',
@@ -69,14 +70,63 @@ public function process($text, $langcode) {
    * {@inheritdoc}
    */
   public function getHTMLRestrictions() {
-    $restrictions = array('allowed' => array());
-    $tags = preg_split('/\s+|<|>/', $this->settings['allowed_html'], -1, PREG_SPLIT_NO_EMPTY);
-    // List the allowed HTML tags.
-    foreach ($tags as $tag) {
-      $restrictions['allowed'][$tag] = TRUE;
+    $restrictions = ['allowed' => []];
+
+    // Parse the allowed HTML setting, and gradually make the whitelist more
+    // specific.
+    $matches = [];
+    preg_match_all('(<[^>]+>)', $this->settings['allowed_html'], $matches);
+    $allowed_tags = $matches[0];
+    foreach ($allowed_tags as $allowed_tag) {
+      $node = Html::load($allowed_tag)->getElementsByTagName('body')->item(0)->firstChild;
+      // First, mark the tag as allowed, but with no attributes allowed.
+      $tag = $node->tagName;
+      $restrictions['allowed'][$tag] = FALSE;
+      // Second, get the attributes, and if any exist, prepare for allowing
+      // specific attributes.
+      $attributes = $node->attributes;
+      if ($attributes->length) {
+        $restrictions['allowed'][$tag] = [];
+      }
+      // Third, iterate over the attributes, and mark them as allowed.
+      foreach ($node->attributes as $name => $attribute) {
+        $restrictions['allowed'][$tag][$name] = TRUE;
+        // Fourth, if the attribute value is not the empty string, this means an
+        // actual attribute value is assigned, mark each of the specified
+        // attribute values as allowed.
+        if ($attribute->value !== '') {
+          $restrictions['allowed'][$tag][$name] = array_filter(explode(' ', $attribute->value));
+        }
+      }
+    }
+
+    // When <a> is allowed and the nofollow setting is enabled, and the rel
+    // attribute is not already generically allowed, then whitelist the 'rel'
+    // attribute, but only allow the 'nofollow' value.
+    if ($this->settings['filter_html_nofollow'] && isset($restrictions['allowed']['a']) && $restrictions['allowed']['a'] !== TRUE) {
+      // If previously no attributes were allowed on <a>, that is now changing.
+      if ($restrictions['allowed']['a'] === FALSE) {
+        $restrictions['allowed']['a'] = [];
+      }
+      // If the 'rel' attribute is not yet whitelisted, then do so now.
+      if (!isset($restrictions['allowed']['a']['rel'])) {
+        $restrictions['allowed']['a']['rel'][] = 'nofollow';
+        $restrictions['allowed']['a']['rel'] = array_unique($restrictions['allowed']['a']['rel']);
+      }
     }
-    // The 'style' and 'on*' ('onClick' etc.) attributes are always forbidden.
-    $restrictions['allowed']['*'] = array('style' => FALSE, 'on*' => FALSE);
+
+    // The 'style' and 'on*' ('onClick' etc.) attributes are always forbidden,
+    // and are removed by Xss::filter().
+    // The 'id', 'lang', and 'dir' attributes apply to all elements and are
+    // always allowed. The value whitelist for the 'dir' attribute is enforced
+    // by _filter_html().
+    $restrictions['allowed']['*'] = [
+      'style' => FALSE,
+      'on*' => FALSE,
+      'id' => TRUE,
+      'lang' => TRUE,
+      'dir' => ['ltr', 'rtl'],
+    ];
     return $restrictions;
   }
 
diff --git a/core/modules/filter/src/Plugin/migrate/process/FilterSettings.php b/core/modules/filter/src/Plugin/migrate/process/FilterSettings.php
new file mode 100644
index 0000000..4ad138a
--- /dev/null
+++ b/core/modules/filter/src/Plugin/migrate/process/FilterSettings.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\migrate\Plugin\migrate\process\FilterSettings.
+ */
+
+namespace Drupal\filter\Plugin\migrate\process;
+
+use Drupal\Component\Transliteration\TransliterationInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\migrate\ProcessPluginBase;
+use Drupal\migrate\MigrateExecutableInterface;
+use Drupal\migrate\Row;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Adds the default allowed attributes to filter_html's allowed_html setting.
+ *
+ * E.g. map '<a>' to '<a href hreflang dir>'.
+ *
+ * @MigrateProcessPlugin(
+ *   id = "filter_settings",
+ *   handle_multiples = TRUE
+ * )
+ */
+class FilterSettings extends ProcessPluginBase {
+
+  /**
+   * Default attributes for migrating filter_html's 'allowed_html' setting.
+   *
+   * @var string[]
+   */
+  protected $allowedHtmlDefaultAttributes = [
+    '<a>' => '<a href hreflang dir>',
+    '<blockquote>' => '<blockquote cite dir>',
+    '<ol>' => '<ol start>',
+    '<h2>' => '<h2 id>',
+    '<h3>' => '<h3 id>',
+    '<h4>' => '<h4 id>',
+    '<h5>' => '<h5 id>',
+    '<h6>' => '<h6 id>',
+    '<p>' => '<p id dir>',
+    '<span>' => '<span dir>',
+    '<img>' => '<img src alt height width>',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
+    // Only the filter_html filter's settings have a changed format.
+    if ($row->getDestinationProperty('id') === 'filter_html') {
+      $value['allowed_html'] = str_replace(array_keys($this->allowedHtmlDefaultAttributes), array_values($this->allowedHtmlDefaultAttributes), $value['allowed_html']);
+    }
+    return $value;
+  }
+
+}
diff --git a/core/modules/filter/src/Tests/FilterAPITest.php b/core/modules/filter/src/Tests/FilterAPITest.php
index 263b31e..1333f28 100644
--- a/core/modules/filter/src/Tests/FilterAPITest.php
+++ b/core/modules/filter/src/Tests/FilterAPITest.php
@@ -108,7 +108,15 @@ function testFilterFormatAPI() {
     $filtered_html_format = entity_load('filter_format', 'filtered_html');
     $this->assertIdentical(
       $filtered_html_format->getHtmlRestrictions(),
-      array('allowed' => array('p' => TRUE, 'br' => TRUE, 'strong' => TRUE, 'a' => TRUE, '*' => array('style' => FALSE, 'on*' => FALSE))),
+      array(
+        'allowed' => array(
+          'p' => FALSE,
+          'br' => FALSE,
+          'strong' => FALSE,
+          'a' => array('href' => TRUE, 'hreflang' => TRUE),
+          '*' => array('style' => FALSE, 'on*' => FALSE, 'id' => TRUE, 'lang' => TRUE, 'dir' => array('ltr', 'rtl')),
+        ),
+      ),
       'FilterFormatInterface::getHtmlRestrictions() works as expected for the filtered_html format.'
     );
     $this->assertIdentical(
@@ -164,7 +172,7 @@ function testFilterFormatAPI() {
         'filter_html' => array(
           'status' => 1,
           'settings' => array(
-            'allowed_html' => '<p> <br> <a> <strong>',
+            'allowed_html' => '<p> <br> <a href> <strong>',
           ),
         ),
         'filter_test_restrict_tags_and_attributes' => array(
@@ -185,7 +193,14 @@ function testFilterFormatAPI() {
     $very_restricted_html_format->save();
     $this->assertIdentical(
       $very_restricted_html_format->getHtmlRestrictions(),
-      array('allowed' => array('p' => TRUE, 'br' => FALSE, 'a' => array('href' => TRUE), '*' => array('style' => FALSE, 'on*' => FALSE))),
+      array(
+        'allowed' => array(
+          'p' => FALSE,
+          'br' => FALSE,
+          'a' => array('href' => TRUE),
+          '*' => array('style' => FALSE, 'on*' => FALSE, 'id' => TRUE, 'lang' => TRUE, 'dir' => array('ltr', 'rtl')),
+        ),
+      ),
       'FilterFormatInterface::getHtmlRestrictions() works as expected for the very_restricted_html format.'
     );
     $this->assertIdentical(
diff --git a/core/modules/filter/src/Tests/FilterHtmlImageSecureTest.php b/core/modules/filter/src/Tests/FilterHtmlImageSecureTest.php
index e320af3..9d96c5f 100644
--- a/core/modules/filter/src/Tests/FilterHtmlImageSecureTest.php
+++ b/core/modules/filter/src/Tests/FilterHtmlImageSecureTest.php
@@ -45,7 +45,7 @@ protected function setUp() {
         'filter_html' => array(
           'status' => 1,
           'settings' => array(
-            'allowed_html' => '<img> <a>',
+            'allowed_html' => '<img src testattribute> <a>',
           ),
         ),
         'filter_autop' => array(
diff --git a/core/modules/filter/src/Tests/FilterUnitTest.php b/core/modules/filter/src/Tests/FilterUnitTest.php
index 9bf0fdb..1ca3d18 100644
--- a/core/modules/filter/src/Tests/FilterUnitTest.php
+++ b/core/modules/filter/src/Tests/FilterUnitTest.php
@@ -190,7 +190,7 @@ function testCaptionFilter() {
     $html_filter = $this->filters['filter_html'];
     $html_filter->setConfiguration(array(
       'settings' => array(
-        'allowed_html' => '<img>',
+        'allowed_html' => '<img src data-align data-caption>',
         'filter_html_help' => 1,
         'filter_html_nofollow' => 0,
       )
@@ -438,10 +438,55 @@ function testHtmlFilter() {
     $this->assertNoNormalized($f, 'onerror', 'HTML filter should remove empty on* attributes on default.');
 
     $f = _filter_html('<br>', $filter);
-    $this->assertNormalized($f, '<br>', 'HTML filter should allow line breaks.');
+    $this->assertNormalized($f, '<br />', 'HTML filter should allow line breaks.');
 
     $f = _filter_html('<br />', $filter);
     $this->assertNormalized($f, '<br />', 'HTML filter should allow self-closing line breaks.');
+
+    // All attributes of whitelisted tags are stripped by default.
+    $f = _filter_html('<a kitten="cute" llama="awesome">link</a>', $filter);
+    $this->assertNormalized($f, '<a>link</a>', 'HTML filter should remove attributes that are not explicitly allowed.');
+
+    // Now whitelist the "llama" attribute on <a>.
+    $filter->setConfiguration(array(
+      'settings' => array(
+        'allowed_html' => '<a href llama> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <br>',
+        'filter_html_help' => 1,
+        'filter_html_nofollow' => 0,
+      )
+    ));
+    $f = _filter_html('<a kitten="cute" llama="awesome">link</a>', $filter);
+    $this->assertNormalized($f, '<a llama="awesome">link</a>', 'HTML filter keeps explicitly allowed attributes, and removes attributes that are not explicitly allowed.');
+
+    // Restrict the whitelisted "llama" attribute on <a> to only allow the value
+    // "majestical".
+    $filter->setConfiguration(array(
+      'settings' => array(
+        'allowed_html' => '<a href llama="majestical"> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <br>',
+        'filter_html_help' => 1,
+        'filter_html_nofollow' => 0,
+      )
+    ));
+    $f = _filter_html('<a kitten="cute" llama="awesome">link</a>', $filter);
+    $this->assertNormalized($f, '<a>link</a>', 'HTML filter removes allowed attributes that have a not explicitly allowed value.');
+    $f = _filter_html('<a kitten="cute" llama="majestical">link</a>', $filter);
+    $this->assertNormalized($f, '<a llama="majestical">link</a>', 'HTML filter keeps explicitly allowed attributes with an attribute value that is also explicitly allowed.');
+
+    // Restrict the whitelisted "llama" attribute on <a> to only allow the value
+    // "majestical" or the value "epic".
+    $filter->setConfiguration(array(
+      'settings' => array(
+        'allowed_html' => '<a href llama="majestical epic"> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <br>',
+        'filter_html_help' => 1,
+        'filter_html_nofollow' => 0,
+      )
+    ));
+    $f = _filter_html('<a kitten="cute" llama="awesome">link</a>', $filter);
+    $this->assertNormalized($f, '<a>link</a>', 'HTML filter removes allowed attributes that have a not explicitly allowed value.');
+    $f = _filter_html('<a kitten="cute" llama="majestical">link</a>', $filter);
+    $this->assertNormalized($f, '<a llama="majestical">link</a>', 'HTML filter keeps explicitly allowed attributes with an attribute value that is also explicitly allowed.');
+    $f = _filter_html('<a kitten="cute" llama="epic">link</a>', $filter);
+    $this->assertNormalized($f, '<a llama="epic">link</a>', 'HTML filter keeps explicitly allowed attributes with an attribute value that is also explicitly allowed.');
   }
 
   /**
@@ -452,7 +497,7 @@ function testNoFollowFilter() {
     $filter = $this->filters['filter_html'];
     $filter->setConfiguration(array(
       'settings' => array(
-        'allowed_html' => '<a>',
+        'allowed_html' => '<a href>',
         'filter_html_help' => 1,
         'filter_html_nofollow' => 1,
       )
diff --git a/core/modules/filter/src/Tests/Migrate/d6/MigrateFilterFormatTest.php b/core/modules/filter/src/Tests/Migrate/d6/MigrateFilterFormatTest.php
index febea32..214897c 100644
--- a/core/modules/filter/src/Tests/Migrate/d6/MigrateFilterFormatTest.php
+++ b/core/modules/filter/src/Tests/Migrate/d6/MigrateFilterFormatTest.php
@@ -48,7 +48,7 @@ public function testFilterFormat() {
     $this->assertFalse(isset($filters['filter_html_image_secure']));
 
     // Check variables migrated into filter.
-    $this->assertIdentical('<a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>', $filters['filter_html']['settings']['allowed_html']);
+    $this->assertIdentical('<a href hreflang dir> <em> <strong> <cite> <code> <ul> <ol start> <li> <dl> <dt> <dd>', $filters['filter_html']['settings']['allowed_html']);
     $this->assertIdentical(TRUE, $filters['filter_html']['settings']['filter_html_help']);
     $this->assertIdentical(FALSE, $filters['filter_html']['settings']['filter_html_nofollow']);
     $this->assertIdentical(72, $filters['filter_url']['settings']['filter_url_length']);
diff --git a/core/modules/filter/tests/filter_test/config/install/filter.format.filtered_html.yml b/core/modules/filter/tests/filter_test/config/install/filter.format.filtered_html.yml
index a1a38d7..ba403db 100644
--- a/core/modules/filter/tests/filter_test/config/install/filter.format.filtered_html.yml
+++ b/core/modules/filter/tests/filter_test/config/install/filter.format.filtered_html.yml
@@ -12,4 +12,4 @@ filters:
     provider: filter
     status: true
     settings:
-      allowed_html: '<p> <br> <strong> <a>'
+      allowed_html: '<p> <br> <strong> <a href hreflang>'
diff --git a/core/profiles/standard/config/install/filter.format.basic_html.yml b/core/profiles/standard/config/install/filter.format.basic_html.yml
index 21a4656..027c953 100644
--- a/core/profiles/standard/config/install/filter.format.basic_html.yml
+++ b/core/profiles/standard/config/install/filter.format.basic_html.yml
@@ -11,7 +11,7 @@ filters:
     status: true
     weight: -10
     settings:
-      allowed_html: '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <h2> <h3> <h4> <h5> <h6> <p> <br> <span> <img>'
+      allowed_html: '<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start> <li> <dl> <dt> <dd> <h2> <h3> <h4> <h5> <h6> <p> <br> <span> <img src alt height width data-entity-type data-entity-uuid data-align data-caption>'
       filter_html_help: false
       filter_html_nofollow: false
   filter_align:
diff --git a/core/profiles/standard/config/install/filter.format.restricted_html.yml b/core/profiles/standard/config/install/filter.format.restricted_html.yml
index 0e40378..c9146d0 100644
--- a/core/profiles/standard/config/install/filter.format.restricted_html.yml
+++ b/core/profiles/standard/config/install/filter.format.restricted_html.yml
@@ -11,7 +11,7 @@ filters:
     status: true
     weight: -10
     settings:
-      allowed_html: '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <h2> <h3> <h4> <h5> <h6>'
+      allowed_html: '<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul> <ol start> <li> <dl> <dt> <dd> <h2> <h3> <h4> <h5> <h6>'
       filter_html_help: true
       filter_html_nofollow: false
   filter_autop:
