diff --git a/core/lib/Drupal/Core/Utility/Token.php b/core/lib/Drupal/Core/Utility/Token.php
index f1f80cc8697acd1c7dec41f7e620d48e42a750df..1b791af3e597bd1ccab5adb414b539fe055ecbf9 100644
--- a/core/lib/Drupal/Core/Utility/Token.php
+++ b/core/lib/Drupal/Core/Utility/Token.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\Utility;
 
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\SafeStringInterface;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Cache\CacheBackendInterface;
@@ -141,7 +143,9 @@ public function __construct(ModuleHandlerInterface $module_handler, CacheBackend
    * Replaces all tokens in a given string with appropriate values.
    *
    * @param string $text
-   *   A string potentially containing replaceable tokens.
+   *   An HTML string containing replaceable tokens. The caller is responsible
+   *   for calling \Drupal\Component\Utility\Html::escape() in case the $text
+   *   was plain text.
    * @param array $data
    *   (optional) An array of keyed objects. For simple replacement scenarios
    *   'node', 'user', and others are common keys, with an accompanying node or
@@ -154,18 +158,9 @@ public function __construct(ModuleHandlerInterface $module_handler, CacheBackend
    *   - langcode: A language code to be used when generating locale-sensitive
    *     tokens.
    *   - callback: A callback function that will be used to post-process the
-   *     array of token replacements after they are generated. For example, a
-   *     module using tokens in a text-only email might provide a callback to
-   *     strip HTML entities from token values before they are inserted into the
-   *     final text.
+   *     array of token replacements after they are generated.
    *   - clear: A boolean flag indicating that tokens should be removed from the
    *     final text if no replacement value can be generated.
-   *   - sanitize: A boolean flag indicating that tokens should be sanitized for
-   *     display to a web browser. Defaults to TRUE. Developers who set this
-   *     option to FALSE assume responsibility for running
-   *     \Drupal\Component\Utility\Xss::filter(),
-   *     \Drupal\Component\Utility\Html::escape() or other appropriate scrubbing
-   *     functions before displaying data to users.
    * @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata|null
    *   (optional) An object to which static::generate() and the hooks and
    *   functions that it invokes will add their required bubbleable metadata.
@@ -185,7 +180,13 @@ public function __construct(ModuleHandlerInterface $module_handler, CacheBackend
    *   Renderer's currently active render context.
    *
    * @return string
-   *   Text with tokens replaced.
+   *   The token result is the entered HTML text with tokens replaced. The
+   *   caller is responsible for choosing the right escaping / sanitization. If
+   *   the result is intended to be used as plain text, using
+   *   PlainTextOutput::renderFromHtml() is recommended. If the result is just
+   *   printed as part of a template relying on Twig autoescaping is possible,
+   *   otherwise for example the result can be put into #markup, in which case
+   *   it would be sanitized by Xss::filterAdmin().
    */
   public function replace($text, array $data = array(), array $options = array(), BubbleableMetadata $bubbleable_metadata = NULL) {
     $text_tokens = $this->scan($text);
@@ -204,6 +205,11 @@ public function replace($text, array $data = array(), array $options = array(),
       }
     }
 
+    // Escape the tokens, unless they are explicitly markup.
+    foreach ($replacements as $token => $value) {
+      $replacements[$token] = $value instanceof SafeStringInterface ? $value : Html::escape($value);
+    }
+
     // Optionally alter the list of replacement values.
     if (!empty($options['callback'])) {
       $function = $options['callback'];
@@ -282,11 +288,6 @@ public function scan($text) {
    *     array of token replacements after they are generated. Can be used when
    *     modules require special formatting of token text, for example URL
    *     encoding or truncation to a specific length.
-   *   - sanitize: A boolean flag indicating that tokens should be sanitized for
-   *     display to a web browser. Developers who set this option to FALSE assume
-   *     responsibility for running \Drupal\Component\Utility\Xss::filter(),
-   *     \Drupal\Component\Utility\Html::escape() or other appropriate scrubbing
-   *     functions before displaying data to users.
    * @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata
    *    The bubbleable metadata. This is passed to the token replacement
    *    implementations so that they can attach their metadata.
@@ -300,8 +301,6 @@ public function scan($text) {
    * @see hook_tokens_alter()
    */
   public function generate($type, array $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
-    $options += array('sanitize' => TRUE);
-
     foreach ($data as $object) {
       if ($object instanceof CacheableDependencyInterface || $object instanceof AttachmentsInterface) {
         $bubbleable_metadata->addCacheableDependency($object);
diff --git a/core/lib/Drupal/Core/Utility/token.api.php b/core/lib/Drupal/Core/Utility/token.api.php
index 57a1e6d192715ebf406422de1e41088b56849cee..4bc619f7da7876af93286241e4715d4352238e91 100644
--- a/core/lib/Drupal/Core/Utility/token.api.php
+++ b/core/lib/Drupal/Core/Utility/token.api.php
@@ -5,7 +5,6 @@
  * Hooks related to the Token system.
  */
 
-use Drupal\Component\Utility\Html;
 use Drupal\user\Entity\User;
 
 /**
@@ -65,7 +64,9 @@
  *
  * @return array
  *   An associative array of replacement values, keyed by the raw [type:token]
- *   strings from the original text.
+ *   strings from the original text. The returned values must be either plain
+ *   text strings, or an object implementing SafeStringInterface if they are
+ *   HTML-formatted.
  *
  * @see hook_token_info()
  * @see hook_tokens_alter()
@@ -81,8 +82,6 @@ function hook_tokens($type, $tokens, array $data, array $options, \Drupal\Core\R
   else {
     $langcode = NULL;
   }
-  $sanitize = !empty($options['sanitize']);
-
   $replacements = array();
 
   if ($type == 'node' && !empty($data['node'])) {
@@ -97,7 +96,7 @@ function hook_tokens($type, $tokens, array $data, array $options, \Drupal\Core\R
           break;
 
         case 'title':
-          $replacements[$original] = $sanitize ? Html::escape($node->getTitle()) : $node->getTitle();
+          $replacements[$original] = $node->getTitle();
           break;
 
         case 'edit-url':
@@ -107,7 +106,7 @@ function hook_tokens($type, $tokens, array $data, array $options, \Drupal\Core\R
         // Default values for the chained tokens handled below.
         case 'author':
           $account = $node->getOwner() ? $node->getOwner() : User::load(0);
-          $replacements[$original] = $sanitize ? Html::escape($account->label()) : $account->label();
+          $replacements[$original] = $account->label();
           $bubbleable_metadata->addCacheableDependency($account);
           break;
 
diff --git a/core/modules/action/src/Plugin/Action/EmailAction.php b/core/modules/action/src/Plugin/Action/EmailAction.php
index 252428c13677c802f83508bd0848347a8b565cc3..4c68e1d0086029a487caa49335c75f629f36dc02 100644
--- a/core/modules/action/src/Plugin/Action/EmailAction.php
+++ b/core/modules/action/src/Plugin/Action/EmailAction.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\action\Plugin\Action;
 
+use Drupal\Component\Utility\PlainTextOutput;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Action\ConfigurableActionBase;
 use Drupal\Core\Entity\EntityManagerInterface;
@@ -127,7 +128,7 @@ public function execute($entity = NULL) {
       $this->configuration['node'] = $entity;
     }
 
-    $recipient = $this->token->replace($this->configuration['recipient'], $this->configuration);
+    $recipient = PlainTextOutput::renderFromHtml($this->token->replace($this->configuration['recipient'], $this->configuration));
 
     // If the recipient is a registered user with a language preference, use
     // the recipient's preferred language. Otherwise, use the system default
diff --git a/core/modules/action/src/Plugin/Action/MessageAction.php b/core/modules/action/src/Plugin/Action/MessageAction.php
index 3da24ef16d96af74594cf3911520496ed05359e1..f754405e64d0c858fa49b949050cfc3b8c72bd30 100644
--- a/core/modules/action/src/Plugin/Action/MessageAction.php
+++ b/core/modules/action/src/Plugin/Action/MessageAction.php
@@ -7,11 +7,11 @@
 
 namespace Drupal\action\Plugin\Action;
 
-use Drupal\Component\Utility\Xss;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Action\ConfigurableActionBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Utility\Token;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -32,20 +32,41 @@ class MessageAction extends ConfigurableActionBase implements ContainerFactoryPl
    */
   protected $token;
 
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
   /**
    * Constructs a MessageAction object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Utility\Token
+   *   The token service.
+   * @param \Drupal\Core\Utility\Token $token
+   *   The token replacement service.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, Token $token) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, Token $token, RendererInterface $renderer) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
 
     $this->token = $token;
+    $this->renderer = $renderer;
   }
 
   /**
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    return new static($configuration, $plugin_id, $plugin_definition, $container->get('token'));
+    return new static($configuration, $plugin_id, $plugin_definition, $container->get('token'), $container->get('renderer'));
   }
 
   /**
@@ -55,8 +76,13 @@ public function execute($entity = NULL) {
     if (empty($this->configuration['node'])) {
       $this->configuration['node'] = $entity;
     }
-    $message = $this->token->replace(Xss::filterAdmin($this->configuration['message']), $this->configuration);
-    drupal_set_message($message);
+    $message = $this->token->replace($this->configuration['message'], $this->configuration);
+    $build = [
+      '#markup' => $message,
+    ];
+
+    // @todo Fix in https://www.drupal.org/node/2577827
+    drupal_set_message($this->renderer->renderPlain($build));
   }
 
   /**
diff --git a/core/modules/comment/comment.tokens.inc b/core/modules/comment/comment.tokens.inc
index 1bd179ee01e18ffd6fc54162c9dd34367f050d5f..e2d606f5dd4aa5e765f73719dfdf774f90dc303a 100644
--- a/core/modules/comment/comment.tokens.inc
+++ b/core/modules/comment/comment.tokens.inc
@@ -5,9 +5,7 @@
  * Builds placeholder replacement tokens for comment-related data.
  */
 
-use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\UrlHelper;
-use Drupal\Component\Utility\Xss;
 use Drupal\Core\Datetime\Entity\DateFormat;
 use Drupal\Core\Render\BubbleableMetadata;
 
@@ -119,8 +117,6 @@ function comment_tokens($type, $tokens, array $data, array $options, BubbleableM
   else {
     $langcode = NULL;
   }
-  $sanitize = !empty($options['sanitize']);
-
   $replacements = array();
 
   if ($type == 'comment' && !empty($data['comment'])) {
@@ -136,7 +132,7 @@ function comment_tokens($type, $tokens, array $data, array $options, BubbleableM
 
         // Poster identity information for comments.
         case 'hostname':
-          $replacements[$original] = $sanitize ? Html::escape($comment->getHostname()) : $comment->getHostname();
+          $replacements[$original] = $comment->getHostname();
           break;
 
         case 'mail':
@@ -146,23 +142,25 @@ function comment_tokens($type, $tokens, array $data, array $options, BubbleableM
           if ($comment->getOwnerId()) {
             $bubbleable_metadata->addCacheableDependency($comment->getOwner());
           }
-          $replacements[$original] = $sanitize ? Html::escape($mail) : $mail;
+          $replacements[$original] = $mail;
           break;
 
         case 'homepage':
-          $replacements[$original] = $sanitize ? UrlHelper::filterBadProtocol($comment->getHomepage()) : $comment->getHomepage();
+          $replacements[$original] = UrlHelper::stripDangerousProtocols($comment->getHomepage());
           break;
 
         case 'title':
-          $replacements[$original] = $sanitize ? Html::escape($comment->getSubject()) : $comment->getSubject();
+          $replacements[$original] = $comment->getSubject();
           break;
 
         case 'body':
-          $replacements[$original] = $sanitize ? $comment->comment_body->processed : $comment->comment_body->value;
+          // "processed" returns a \Drupal\Component\Utility\SafeStringInterface
+          // via check_markup().
+          $replacements[$original] = $comment->comment_body->processed;
           break;
 
         case 'langcode':
-          $replacements[$original] = $sanitize ? Html::escape($comment->language()->getId()) : $comment->language()->getId();
+          $replacements[$original] = $comment->language()->getId();
           break;
 
         // Comment related URLs.
@@ -183,14 +181,14 @@ function comment_tokens($type, $tokens, array $data, array $options, BubbleableM
           if ($comment->getOwnerId()) {
             $bubbleable_metadata->addCacheableDependency($comment->getOwner());
           }
-          $replacements[$original] = $sanitize ? Html::escape($name) : $name;
+          $replacements[$original] = $name;
           break;
 
         case 'parent':
           if ($comment->hasParentComment()) {
             $parent = $comment->getParentComment();
             $bubbleable_metadata->addCacheableDependency($parent);
-            $replacements[$original] = $sanitize ? Html::escape($parent->getSubject()) : $parent->getSubject();
+            $replacements[$original] = $parent->getSubject();
           }
           break;
 
@@ -210,7 +208,7 @@ function comment_tokens($type, $tokens, array $data, array $options, BubbleableM
           $entity = $comment->getCommentedEntity();
           $bubbleable_metadata->addCacheableDependency($entity);
           $title = $entity->label();
-          $replacements[$original] = $sanitize ? Html::escape($title) : $title;
+          $replacements[$original] = $title;
           break;
       }
     }
diff --git a/core/modules/comment/src/Tests/CommentTokenReplaceTest.php b/core/modules/comment/src/Tests/CommentTokenReplaceTest.php
index cf9699805910eaadd7f4c9cfcfa8bf48a6079a15..13c415e7d2db1fc858109097992d310104bbc0b2 100644
--- a/core/modules/comment/src/Tests/CommentTokenReplaceTest.php
+++ b/core/modules/comment/src/Tests/CommentTokenReplaceTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\comment\Tests;
 
+use Drupal\Component\Utility\FormattableString;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Component\Utility\Xss;
@@ -32,13 +33,16 @@ function testCommentTokenReplacement() {
       'language' => $language_interface,
     );
 
+    // Change the title of the admin user.
+    $this->adminUser->name->value = 'This is a title with some special & > " stuff.';
+    $this->adminUser->save();
     $this->drupalLogin($this->adminUser);
 
     // Set comment variables.
     $this->setCommentSubject(TRUE);
 
     // Create a node and a comment.
-    $node = $this->drupalCreateNode(array('type' => 'article'));
+    $node = $this->drupalCreateNode(['type' => 'article', 'title' => '<script>alert("123")</script>']);
     $parent_comment = $this->postComment($node, $this->randomMachineName(), $this->randomMachineName(), TRUE);
 
     // Post a reply to the comment.
@@ -50,29 +54,29 @@ function testCommentTokenReplacement() {
     // Add HTML to ensure that sanitation of some fields tested directly.
     $comment->setSubject('<blink>Blinking Comment</blink>');
 
-    // Generate and test sanitized tokens.
+    // Generate and test tokens.
     $tests = array();
     $tests['[comment:cid]'] = $comment->id();
-    $tests['[comment:hostname]'] = Html::escape($comment->getHostname());
+    $tests['[comment:hostname]'] = $comment->getHostname();
     $tests['[comment:author]'] = Html::escape($comment->getAuthorName());
-    $tests['[comment:mail]'] = Html::escape($this->adminUser->getEmail());
+    $tests['[comment:mail]'] = $this->adminUser->getEmail();
     $tests['[comment:homepage]'] = UrlHelper::filterBadProtocol($comment->getHomepage());
     $tests['[comment:title]'] = Html::escape($comment->getSubject());
     $tests['[comment:body]'] = $comment->comment_body->processed;
-    $tests['[comment:langcode]'] = Html::escape($comment->language()->getId());
+    $tests['[comment:langcode]'] = $comment->language()->getId();
     $tests['[comment:url]'] = $comment->url('canonical', $url_options + array('fragment' => 'comment-' . $comment->id()));
     $tests['[comment:edit-url]'] = $comment->url('edit-form', $url_options);
     $tests['[comment:created]'] = \Drupal::service('date.formatter')->format($comment->getCreatedTime(), 'medium', array('langcode' => $language_interface->getId()));
     $tests['[comment:created:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($comment->getCreatedTime(), array('langcode' => $language_interface->getId()));
     $tests['[comment:changed:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($comment->getChangedTimeAcrossTranslations(), array('langcode' => $language_interface->getId()));
     $tests['[comment:parent:cid]'] = $comment->hasParentComment() ? $comment->getParentComment()->id() : NULL;
-    $tests['[comment:parent:title]'] = Html::escape($parent_comment->getSubject());
+    $tests['[comment:parent:title]'] = $parent_comment->getSubject();
     $tests['[comment:entity]'] = Html::escape($node->getTitle());
     // Test node specific tokens.
     $tests['[comment:entity:nid]'] = $comment->getCommentedEntityId();
     $tests['[comment:entity:title]'] = Html::escape($node->getTitle());
     $tests['[comment:author:uid]'] = $comment->getOwnerId();
-    $tests['[comment:author:name]'] = Html::escape($this->adminUser->getUsername());
+    $tests['[comment:author:name]'] = Html::escape($this->adminUser->getDisplayName());
 
     $base_bubbleable_metadata = BubbleableMetadata::createFromObject($comment);
     $metadata_tests = [];
@@ -114,35 +118,16 @@ function testCommentTokenReplacement() {
     foreach ($tests as $input => $expected) {
       $bubbleable_metadata = new BubbleableMetadata();
       $output = $token_service->replace($input, array('comment' => $comment), array('langcode' => $language_interface->getId()), $bubbleable_metadata);
-      $this->assertEqual($output, $expected, format_string('Sanitized comment token %token replaced.', array('%token' => $input)));
+      $this->assertEqual($output, $expected, new FormattableString('Comment token %token replaced.', ['%token' => $input]));
       $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]);
     }
 
-    // Generate and test unsanitized tokens.
-    $tests['[comment:hostname]'] = $comment->getHostname();
-    $tests['[comment:author]'] = $comment->getAuthorName();
-    $tests['[comment:mail]'] = $this->adminUser->getEmail();
-    $tests['[comment:homepage]'] = $comment->getHomepage();
-    $tests['[comment:title]'] = $comment->getSubject();
-    $tests['[comment:body]'] = $comment->comment_body->value;
-    $tests['[comment:langcode]'] = $comment->language()->getId();
-    $tests['[comment:parent:title]'] = $parent_comment->getSubject();
-    $tests['[comment:entity]'] = $node->getTitle();
-    $tests['[comment:author:name]'] = $this->adminUser->getUsername();
-
-    foreach ($tests as $input => $expected) {
-      $output = $token_service->replace($input, array('comment' => $comment), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE));
-      $this->assertEqual($output, $expected, format_string('Unsanitized comment token %token replaced.', array('%token' => $input)));
-    }
-
     // Test anonymous comment author.
-    $author_name = $this->randomString();
+    $author_name = 'This is a random & " > string';
     $comment->setOwnerId(0)->setAuthorName($author_name);
     $input = '[comment:author]';
     $output = $token_service->replace($input, array('comment' => $comment), array('langcode' => $language_interface->getId()));
-    $this->assertEqual($output, Html::escape($author_name), format_string('Sanitized comment author token %token replaced.', array('%token' => $input)));
-    $output = $token_service->replace($input, array('comment' => $comment), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE));
-    $this->assertEqual($output, $author_name, format_string('Unsanitized comment author token %token replaced.', array('%token' => $input)));
+    $this->assertEqual($output, Html::escape($author_name), format_string('Comment author token %token replaced.', array('%token' => $input)));
 
     // Load node so comment_count gets computed.
     $node = Node::load($node->id());
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index 3c677ea1681ca6109dc74e7a19887f1dfa5de635..ec8556f38d6f9fc3fcb6c47080a1694d2c814ee1 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -5,7 +5,6 @@
  * Defines a "managed_file" Form API field and a "file" field for Field module.
  */
 
-use Drupal\Component\Utility\Html;
 use Drupal\Core\Datetime\Entity\DateFormat;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Form\FormStateInterface;
@@ -951,7 +950,6 @@ function file_tokens($type, $tokens, array $data, array $options, BubbleableMeta
   else {
     $langcode = NULL;
   }
-  $sanitize = !empty($options['sanitize']);
 
   $replacements = array();
 
@@ -968,15 +966,15 @@ function file_tokens($type, $tokens, array $data, array $options, BubbleableMeta
 
         // Essential file data
         case 'name':
-          $replacements[$original] = $sanitize ? Html::escape($file->getFilename()) : $file->getFilename();
+          $replacements[$original] = $file->getFilename();
           break;
 
         case 'path':
-          $replacements[$original] = $sanitize ? Html::escape($file->getFileUri()) : $file->getFileUri();
+          $replacements[$original] = $file->getFileUri();
           break;
 
         case 'mime':
-          $replacements[$original] = $sanitize ? Html::escape($file->getMimeType()) : $file->getMimeType();
+          $replacements[$original] = $file->getMimeType();
           break;
 
         case 'size':
@@ -984,7 +982,7 @@ function file_tokens($type, $tokens, array $data, array $options, BubbleableMeta
           break;
 
         case 'url':
-          $replacements[$original] = $sanitize ? Html::escape(file_create_url($file->getFileUri())) : file_create_url($file->getFileUri());
+          $replacements[$original] = file_create_url($file->getFileUri());
           break;
 
         // These tokens are default variations on the chained tokens handled below.
@@ -1004,7 +1002,7 @@ function file_tokens($type, $tokens, array $data, array $options, BubbleableMeta
           $owner = $file->getOwner();
           $bubbleable_metadata->addCacheableDependency($owner);
           $name = $owner->label();
-          $replacements[$original] = $sanitize ? Html::escape($name) : $name;
+          $replacements[$original] = $name;
           break;
       }
     }
diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php
index afff2a3b111c373e3cdc18d7b3b6bc4552b41594..9174b0feec1672f42dcf3b879aae41f7eda1a20a 100644
--- a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php
+++ b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php
@@ -8,6 +8,7 @@
 namespace Drupal\file\Plugin\Field\FieldType;
 
 use Drupal\Component\Utility\Bytes;
+use Drupal\Component\Utility\PlainTextOutput;
 use Drupal\Component\Utility\Random;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
@@ -260,7 +261,7 @@ public static function validateMaxFilesize($element, FormStateInterface $form_st
    *   An array of token objects to pass to token_replace().
    *
    * @return string
-   *   A file directory URI with tokens replaced.
+   *   An unsanitized file directory URI with tokens replaced.
    *
    * @see token_replace()
    */
@@ -268,9 +269,13 @@ public function getUploadLocation($data = array()) {
     $settings = $this->getSettings();
     $destination = trim($settings['file_directory'], '/');
 
-    // Replace tokens.
-    $destination = \Drupal::token()->replace($destination, $data);
+    // Replace tokens. As the tokens might contain HTML we convert it to plain
+    // text.
+    $destination = PlainTextOutput::renderFromHtml(\Drupal::token()->replace($destination, $data));
 
+    // @todo Is any valid URI always safe output? If not, handle invalid URIs
+    //   here, and certainly do not return them, see
+    //   https://www.drupal.org/node/2578193.
     return $settings['uri_scheme'] . '://' . $destination;
   }
 
diff --git a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
index d9780580e11c6498ad3d0a158eaf6280c05b861f..0bfabb5ea77425448832e5ce80745dbd404e89e9 100644
--- a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
+++ b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
@@ -188,9 +188,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
 
       // If the title field value is available, use it for the link text.
       if (empty($settings['url_only']) && !empty($item->title)) {
-        // Unsanitized token replacement here because the entire link title
-        // gets auto-escaped during link generation.
-        $link_title = \Drupal::token()->replace($item->title, array($entity->getEntityTypeId() => $entity), array('sanitize' => FALSE, 'clear' => TRUE));
+        $link_title = \Drupal::token()->replace($item->title, [$entity->getEntityTypeId() => $entity], ['clear' => TRUE]);
       }
 
       // Trim the link text to the desired length.
diff --git a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkSeparateFormatter.php b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkSeparateFormatter.php
index 5e3f458db8267fa75fe5a9c2d8beee8a6b1ac50a..c6cf2123ae017551bcc508a610775dbb5bf00694 100644
--- a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkSeparateFormatter.php
+++ b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkSeparateFormatter.php
@@ -54,9 +54,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
 
       // If the link text field value is available, use it for the text.
       if (empty($settings['url_only']) && !empty($item->title)) {
-        // Unsanitized token replacement here because the entire link title
-        // gets auto-escaped during link generation.
-        $link_title = \Drupal::token()->replace($item->title, array($entity->getEntityTypeId() => $entity), array('sanitize' => FALSE, 'clear' => TRUE));
+        $link_title = \Drupal::token()->replace($item->title, [$entity->getEntityTypeId() => $entity], ['clear' => TRUE]);
       }
 
       // The link_separate formatter has two titles; the link text (as in the
diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc
index 3294044deb86ba55fac9a2a0094b0c0a1872cdcd..dbe8da29ce897370e96c16e1af269a1243b3edae 100644
--- a/core/modules/node/node.tokens.inc
+++ b/core/modules/node/node.tokens.inc
@@ -5,7 +5,6 @@
  * Builds placeholder replacement tokens for node-related data.
  */
 
-use Drupal\Component\Utility\Html;
 use Drupal\Core\Datetime\Entity\DateFormat;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Render\BubbleableMetadata;
@@ -96,8 +95,6 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta
   else {
     $langcode = LanguageInterface::LANGCODE_DEFAULT;
   }
-  $sanitize = !empty($options['sanitize']);
-
   $replacements = array();
 
   if ($type == 'node' && !empty($data['node'])) {
@@ -116,16 +113,16 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta
           break;
 
         case 'type':
-          $replacements[$original] = $sanitize ? Html::escape($node->getType()) : $node->getType();
+          $replacements[$original] = $node->getType();
           break;
 
         case 'type-name':
           $type_name = node_get_type_label($node);
-          $replacements[$original] = $sanitize ? Html::escape($type_name) : $type_name;
+          $replacements[$original] = $type_name;
           break;
 
         case 'title':
-          $replacements[$original] = $sanitize ? Html::escape($node->getTitle()) : $node->getTitle();
+          $replacements[$original] = $node->getTitle();
           break;
 
         case 'body':
@@ -133,14 +130,13 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta
           $translation = \Drupal::entityManager()->getTranslationFromContext($node, $langcode, array('operation' => 'node_tokens'));
           if ($translation->hasField('body') && ($items = $translation->get('body')) && !$items->isEmpty()) {
             $item = $items[0];
-            $field_definition = \Drupal::entityManager()->getFieldDefinitions('node', $node->bundle())['body'];
             // If the summary was requested and is not empty, use it.
             if ($name == 'summary' && !empty($item->summary)) {
-              $output = $sanitize ? $item->summary_processed : $item->summary;
+              $output = $item->summary_processed;
             }
             // Attempt to provide a suitable version of the 'body' field.
             else {
-              $output = $sanitize ? $item->processed : $item->value;
+              $output = $item->processed;
               // A summary was requested.
               if ($name == 'summary') {
                 // Generate an optionally trimmed summary of the body field.
@@ -159,12 +155,14 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta
                 $output = text_summary($output, $item->format, $length);
               }
             }
+            // "processed" returns a
+            // \Drupal\Component\Utility\SafeStringInterface via check_markup().
             $replacements[$original] = $output;
           }
           break;
 
         case 'langcode':
-          $replacements[$original] = $sanitize ? Html::escape($node->language()->getId()) : $node->language()->getId();
+          $replacements[$original] = $node->language()->getId();
           break;
 
         case 'url':
@@ -179,7 +177,7 @@ function node_tokens($type, $tokens, array $data, array $options, BubbleableMeta
         case 'author':
           $account = $node->getOwner() ? $node->getOwner() : User::load(0);
           $bubbleable_metadata->addCacheableDependency($account);
-          $replacements[$original] = $sanitize ? Html::escape($account->label()) : $account->label();
+          $replacements[$original] = $account->label();
           break;
 
         case 'created':
diff --git a/core/modules/node/src/Tests/NodeTokenReplaceTest.php b/core/modules/node/src/Tests/NodeTokenReplaceTest.php
index 3ec91d9a5ce91b68240da47e028f6fc153238dd1..a99637f5b7f353d3d37f4ac53f74f7e281b2fa26 100644
--- a/core/modules/node/src/Tests/NodeTokenReplaceTest.php
+++ b/core/modules/node/src/Tests/NodeTokenReplaceTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\node\Tests;
 
+use Drupal\Component\Utility\FormattableString;
 use Drupal\Component\Utility\Html;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\system\Tests\System\TokenReplaceUnitTestBase;
@@ -55,11 +56,11 @@ function testNodeTokenReplacement() {
       'tnid' => 0,
       'uid' => $account->id(),
       'title' => '<blink>Blinking Text</blink>',
-      'body' => array(array('value' => $this->randomMachineName(32), 'summary' => $this->randomMachineName(16), 'format' => 'plain_text')),
+      'body' => [['value' => 'Regular NODE body for the test.', 'summary' => 'Fancy NODE summary.', 'format' => 'plain_text']],
     ));
     $node->save();
 
-    // Generate and test sanitized tokens.
+    // Generate and test tokens.
     $tests = array();
     $tests['[node:nid]'] = $node->id();
     $tests['[node:vid]'] = $node->getRevisionId();
@@ -68,12 +69,12 @@ function testNodeTokenReplacement() {
     $tests['[node:title]'] = Html::escape($node->getTitle());
     $tests['[node:body]'] = $node->body->processed;
     $tests['[node:summary]'] = $node->body->summary_processed;
-    $tests['[node:langcode]'] = Html::escape($node->language()->getId());
+    $tests['[node:langcode]'] = $node->language()->getId();
     $tests['[node:url]'] = $node->url('canonical', $url_options);
     $tests['[node:edit-url]'] = $node->url('edit-form', $url_options);
-    $tests['[node:author]'] = Html::escape($account->getUsername());
+    $tests['[node:author]'] = $account->getUsername();
     $tests['[node:author:uid]'] = $node->getOwnerId();
-    $tests['[node:author:name]'] = Html::escape($account->getUsername());
+    $tests['[node:author:name]'] = $account->getUsername();
     $tests['[node:created:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($node->getCreatedTime(), array('langcode' => $this->interfaceLanguage->getId()));
     $tests['[node:changed:since]'] = \Drupal::service('date.formatter')->formatTimeDiffSince($node->getChangedTime(), array('langcode' => $this->interfaceLanguage->getId()));
 
@@ -104,32 +105,20 @@ function testNodeTokenReplacement() {
     foreach ($tests as $input => $expected) {
       $bubbleable_metadata = new BubbleableMetadata();
       $output = $this->tokenService->replace($input, array('node' => $node), array('langcode' => $this->interfaceLanguage->getId()), $bubbleable_metadata);
-      $this->assertEqual($output, $expected, format_string('Sanitized node token %token replaced.', array('%token' => $input)));
+      $this->assertEqual($output, $expected, format_string('Node token %token replaced.', ['%token' => $input]));
       $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]);
     }
 
-    // Generate and test unsanitized tokens.
-    $tests['[node:title]'] = $node->getTitle();
-    $tests['[node:body]'] = $node->body->value;
-    $tests['[node:summary]'] = $node->body->summary;
-    $tests['[node:langcode]'] = $node->language()->getId();
-    $tests['[node:author:name]'] = $account->getUsername();
-
-    foreach ($tests as $input => $expected) {
-      $output = $this->tokenService->replace($input, array('node' => $node), array('langcode' => $this->interfaceLanguage->getId(), 'sanitize' => FALSE));
-      $this->assertEqual($output, $expected, format_string('Unsanitized node token %token replaced.', array('%token' => $input)));
-    }
-
     // Repeat for a node without a summary.
     $node = entity_create('node', array(
       'type' => 'article',
       'uid' => $account->id(),
       'title' => '<blink>Blinking Text</blink>',
-      'body' => array(array('value' => $this->randomMachineName(32), 'format' => 'plain_text')),
+      'body' => [['value' => 'A string that looks random like TR5c2I', 'format' => 'plain_text']],
     ));
     $node->save();
 
-    // Generate and test sanitized token - use full body as expected value.
+    // Generate and test token - use full body as expected value.
     $tests = array();
     $tests['[node:summary]'] = $node->body->processed;
 
@@ -138,15 +127,7 @@ function testNodeTokenReplacement() {
 
     foreach ($tests as $input => $expected) {
       $output = $this->tokenService->replace($input, array('node' => $node), array('language' => $this->interfaceLanguage));
-      $this->assertEqual($output, $expected, format_string('Sanitized node token %token replaced for node without a summary.', array('%token' => $input)));
-    }
-
-    // Generate and test unsanitized tokens.
-    $tests['[node:summary]'] = $node->body->value;
-
-    foreach ($tests as $input => $expected) {
-      $output = $this->tokenService->replace($input, array('node' => $node), array('language' => $this->interfaceLanguage, 'sanitize' => FALSE));
-      $this->assertEqual($output, $expected, format_string('Unsanitized node token %token replaced for node without a summary.', array('%token' => $input)));
+      $this->assertEqual($output, $expected, new FormattableString('Node token %token replaced for node without a summary.', ['%token' => $input]));
     }
   }
 
diff --git a/core/modules/system/src/Tests/System/TokenReplaceUnitTest.php b/core/modules/system/src/Tests/System/TokenReplaceUnitTest.php
index 33f867a522beb54620e0a4a2f9efcdbdd775ca93..c7af02ddf69bd7783e599c797deecf503c350772 100644
--- a/core/modules/system/src/Tests/System/TokenReplaceUnitTest.php
+++ b/core/modules/system/src/Tests/System/TokenReplaceUnitTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\System;
 
+use Drupal\Component\Utility\FormattableString;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Render\BubbleableMetadata;
@@ -103,7 +104,7 @@ public function testSystemSiteTokenReplacement() {
       ->save();
 
 
-    // Generate and test sanitized tokens.
+    // Generate and test tokens.
     $tests = array();
     $tests['[site:name]'] = Html::escape($config->get('name'));
     $tests['[site:slogan]'] = $safe_slogan;
@@ -129,29 +130,9 @@ public function testSystemSiteTokenReplacement() {
     foreach ($tests as $input => $expected) {
       $bubbleable_metadata = new BubbleableMetadata();
       $output = $this->tokenService->replace($input, array(), array('langcode' => $this->interfaceLanguage->getId()), $bubbleable_metadata);
-      $this->assertEqual($output, $expected, format_string('Sanitized system site information token %token replaced.', array('%token' => $input)));
+      $this->assertEqual($output, $expected, new FormattableString('System site information token %token replaced.', ['%token' => $input]));
       $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]);
     }
-
-    // Generate and test unsanitized tokens.
-    $tests['[site:name]'] = $config->get('name');
-    $tests['[site:slogan]'] = $config->get('slogan');
-
-    foreach ($tests as $input => $expected) {
-      $output = $this->tokenService->replace($input, array(), array('langcode' => $this->interfaceLanguage->getId(), 'sanitize' => FALSE), $bubbleable_metadata);
-      $this->assertEqual($output, $expected, format_string('Unsanitized system site information token %token replaced.', array('%token' => $input)));
-    }
-
-    // Check that the results of Token::generate are sanitized properly. This
-    // does NOT test the cleanliness of every token -- just that the $sanitize
-    // flag is being passed properly through the call stack and being handled
-    // correctly by a 'known' token, [site:slogan].
-    $raw_tokens = array('slogan' => '[site:slogan]');
-    $generated = $this->tokenService->generate('site', $raw_tokens, [], [], $bubbleable_metadata);
-    $this->assertEqual($generated['[site:slogan]'], $safe_slogan, 'Token sanitized.');
-
-    $generated = $this->tokenService->generate('site', $raw_tokens, array(), array('sanitize' => FALSE), $bubbleable_metadata);
-    $this->assertEqual($generated['[site:slogan]'], $slogan, 'Unsanitized token generated properly.');
   }
 
   /**
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index a8477d3873388e3fa3e4f6cbb3718df41cb58d6b..e21c2c8d268bcdc69a21bad0ff83d637d6cc328c 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -5,6 +5,7 @@
  * Configuration system that lets administrators modify the workings of the site.
  */
 
+use Drupal\Component\Utility\PlainTextOutput;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Asset\AttachedAssetsInterface;
 use Drupal\Core\Cache\Cache;
@@ -1290,7 +1291,7 @@ function system_mail($key, &$message, $params) {
 
   $context = $params['context'];
 
-  $subject = $token_service->replace($context['subject'], $context);
+  $subject = PlainTextOutput::renderFromHtml($token_service->replace($context['subject'], $context));
   $body = $token_service->replace($context['message'], $context);
 
   $message['subject'] .= str_replace(array("\r", "\n"), '', $subject);
diff --git a/core/modules/system/system.tokens.inc b/core/modules/system/system.tokens.inc
index a11f55d0c8f26f79f1b09f688f47c26245c59840..66b5c7faf3f0bc2513c97205854091b982fb99fc 100644
--- a/core/modules/system/system.tokens.inc
+++ b/core/modules/system/system.tokens.inc
@@ -7,8 +7,6 @@
  * This file handles tokens for the global 'site' and 'date' tokens.
  */
 
-use Drupal\Component\Utility\Html;
-use Drupal\Component\Utility\Xss;
 use Drupal\Core\Datetime\Entity\DateFormat;
 use Drupal\Core\Render\BubbleableMetadata;
 
@@ -100,8 +98,6 @@ function system_tokens($type, $tokens, array $data, array $options, BubbleableMe
   else {
     $langcode = NULL;
   }
-  $sanitize = !empty($options['sanitize']);
-
   $replacements = array();
 
   if ($type == 'site') {
@@ -111,14 +107,18 @@ function system_tokens($type, $tokens, array $data, array $options, BubbleableMe
           $config = \Drupal::config('system.site');
           $bubbleable_metadata->addCacheableDependency($config);
           $site_name = $config->get('name');
-          $replacements[$original] = $sanitize ? Html::escape($site_name) : $site_name;
+          $replacements[$original] = $site_name;
           break;
 
         case 'slogan':
           $config = \Drupal::config('system.site');
           $bubbleable_metadata->addCacheableDependency($config);
           $slogan = $config->get('slogan');
-          $replacements[$original] = $sanitize ? Xss::filterAdmin($slogan) : $slogan;
+          $build = [
+            '#markup' => $slogan,
+          ];
+          // @todo Fix in https://www.drupal.org/node/2577827
+          $replacements[$original] = \Drupal::service('renderer')->renderPlain($build);
           break;
 
         case 'mail':
@@ -178,7 +178,7 @@ function system_tokens($type, $tokens, array $data, array $options, BubbleableMe
           break;
 
         case 'raw':
-          $replacements[$original] = $sanitize ? Html::escape($date) : $date;
+          $replacements[$original] = $date;
           break;
       }
     }
diff --git a/core/modules/taxonomy/src/Tests/TokenReplaceTest.php b/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
index 4fcb0f1f9aadd31ba29f94276f06215cbf582007..1e00a8e39ab8eacfe25d337f12b93d2cd8e8ac40 100644
--- a/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
+++ b/core/modules/taxonomy/src/Tests/TokenReplaceTest.php
@@ -7,8 +7,6 @@
 
 namespace Drupal\taxonomy\Tests;
 
-use Drupal\Component\Utility\Html;
-use Drupal\Component\Utility\Xss;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Render\BubbleableMetadata;
 
@@ -86,13 +84,13 @@ function testTaxonomyTokenReplacement() {
     // Generate and test sanitized tokens for term1.
     $tests = array();
     $tests['[term:tid]'] = $term1->id();
-    $tests['[term:name]'] = Html::escape($term1->getName());
+    $tests['[term:name]'] = $term1->getName();
     $tests['[term:description]'] = $term1->description->processed;
     $tests['[term:url]'] = $term1->url('canonical', array('absolute' => TRUE));
     $tests['[term:node-count]'] = 0;
     $tests['[term:parent:name]'] = '[term:parent:name]';
-    $tests['[term:vocabulary:name]'] = Html::escape($this->vocabulary->label());
-    $tests['[term:vocabulary]'] = Html::escape($this->vocabulary->label());
+    $tests['[term:vocabulary:name]'] = $this->vocabulary->label();
+    $tests['[term:vocabulary]'] = $this->vocabulary->label();
 
     $base_bubbleable_metadata = BubbleableMetadata::createFromObject($term1);
 
@@ -117,14 +115,14 @@ function testTaxonomyTokenReplacement() {
     // Generate and test sanitized tokens for term2.
     $tests = array();
     $tests['[term:tid]'] = $term2->id();
-    $tests['[term:name]'] = Html::escape($term2->getName());
+    $tests['[term:name]'] = $term2->getName();
     $tests['[term:description]'] = $term2->description->processed;
     $tests['[term:url]'] = $term2->url('canonical', array('absolute' => TRUE));
     $tests['[term:node-count]'] = 1;
-    $tests['[term:parent:name]'] = Html::escape($term1->getName());
+    $tests['[term:parent:name]'] = $term1->getName();
     $tests['[term:parent:url]'] = $term1->url('canonical', array('absolute' => TRUE));
     $tests['[term:parent:parent:name]'] = '[term:parent:parent:name]';
-    $tests['[term:vocabulary:name]'] = Html::escape($this->vocabulary->label());
+    $tests['[term:vocabulary:name]'] = $this->vocabulary->label();
 
     // Test to make sure that we generated something for each token.
     $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.');
@@ -134,22 +132,11 @@ function testTaxonomyTokenReplacement() {
       $this->assertEqual($output, $expected, format_string('Sanitized taxonomy term token %token replaced.', array('%token' => $input)));
     }
 
-    // Generate and test unsanitized tokens.
-    $tests['[term:name]'] = $term2->getName();
-    $tests['[term:description]'] = $term2->getDescription();
-    $tests['[term:parent:name]'] = $term1->getName();
-    $tests['[term:vocabulary:name]'] = $this->vocabulary->label();
-
-    foreach ($tests as $input => $expected) {
-      $output = $token_service->replace($input, array('term' => $term2), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE));
-      $this->assertEqual($output, $expected, format_string('Unsanitized taxonomy term token %token replaced.', array('%token' => $input)));
-    }
-
     // Generate and test sanitized tokens.
     $tests = array();
     $tests['[vocabulary:vid]'] = $this->vocabulary->id();
-    $tests['[vocabulary:name]'] = Html::escape($this->vocabulary->label());
-    $tests['[vocabulary:description]'] = Xss::filter($this->vocabulary->getDescription());
+    $tests['[vocabulary:name]'] = $this->vocabulary->label();
+    $tests['[vocabulary:description]'] = $this->vocabulary->getDescription();
     $tests['[vocabulary:node-count]'] = 1;
     $tests['[vocabulary:term-count]'] = 2;
 
@@ -160,15 +147,6 @@ function testTaxonomyTokenReplacement() {
       $output = $token_service->replace($input, array('vocabulary' => $this->vocabulary), array('langcode' => $language_interface->getId()));
       $this->assertEqual($output, $expected, format_string('Sanitized taxonomy vocabulary token %token replaced.', array('%token' => $input)));
     }
-
-    // Generate and test unsanitized tokens.
-    $tests['[vocabulary:name]'] = $this->vocabulary->label();
-    $tests['[vocabulary:description]'] = $this->vocabulary->getDescription();
-
-    foreach ($tests as $input => $expected) {
-      $output = $token_service->replace($input, array('vocabulary' => $this->vocabulary), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE));
-      $this->assertEqual($output, $expected, format_string('Unsanitized taxonomy vocabulary token %token replaced.', array('%token' => $input)));
-    }
   }
 }
 
diff --git a/core/modules/taxonomy/taxonomy.tokens.inc b/core/modules/taxonomy/taxonomy.tokens.inc
index daf690b2ba3fc837682dd75389713d0d86a4226c..aac7acbbf5dc5fc4d4aaf7e672243e6be4d9835f 100644
--- a/core/modules/taxonomy/taxonomy.tokens.inc
+++ b/core/modules/taxonomy/taxonomy.tokens.inc
@@ -5,8 +5,6 @@
  * Builds placeholder replacement tokens for taxonomy terms and vocabularies.
  */
 
-use Drupal\Component\Utility\Html;
-use Drupal\Component\Utility\Xss;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\taxonomy\Entity\Vocabulary;
 
@@ -97,7 +95,6 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable
   $token_service = \Drupal::token();
 
   $replacements = array();
-  $sanitize = !empty($options['sanitize']);
   $taxonomy_storage = \Drupal::entityManager()->getStorage('taxonomy_term');
   if ($type == 'term' && !empty($data['term'])) {
     $term = $data['term'];
@@ -109,11 +106,13 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable
           break;
 
         case 'name':
-          $replacements[$original] = $sanitize ? Html::escape($term->getName()) : $term->getName();
+          $replacements[$original] = $term->getName();
           break;
 
         case 'description':
-          $replacements[$original] = $sanitize ? $term->description->processed : $term->getDescription();
+          // "processed" returns a \Drupal\Component\Utility\SafeStringInterface
+          // via check_markup().
+          $replacements[$original] = $term->description->processed;
           break;
 
         case 'url':
@@ -131,14 +130,14 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable
         case 'vocabulary':
           $vocabulary = Vocabulary::load($term->bundle());
           $bubbleable_metadata->addCacheableDependency($vocabulary);
-          $replacements[$original] = Html::escape($vocabulary->label());
+          $replacements[$original] = $vocabulary->label();
           break;
 
         case 'parent':
           if ($parents = $taxonomy_storage->loadParents($term->id())) {
             $parent = array_pop($parents);
             $bubbleable_metadata->addCacheableDependency($parent);
-            $replacements[$original] = Html::escape($parent->getName());
+            $replacements[$original] = $parent->getName();
           }
           break;
       }
@@ -165,11 +164,13 @@ function taxonomy_tokens($type, $tokens, array $data, array $options, Bubbleable
           break;
 
         case 'name':
-          $replacements[$original] = $sanitize ? Html::escape($vocabulary->label()) : $vocabulary->label();
+          $replacements[$original] = $vocabulary->label();
           break;
 
         case 'description':
-          $replacements[$original] = $sanitize ? Xss::filter($vocabulary->getDescription()) : $vocabulary->getDescription();
+          $build = ['#markup' => $vocabulary->getDescription()];
+          // @todo Fix in https://www.drupal.org/node/2577827
+          $replacements[$original] = \Drupal::service('renderer')->renderPlain($build);
           break;
 
         case 'term-count':
diff --git a/core/modules/tour/src/Plugin/tour/tip/TipPluginText.php b/core/modules/tour/src/Plugin/tour/tip/TipPluginText.php
index bedb4bd24273097724639f37e5abf061c8792702..f525d83e327a314fff70a2eccaacbccc50c99bfc 100644
--- a/core/modules/tour/src/Plugin/tour/tip/TipPluginText.php
+++ b/core/modules/tour/src/Plugin/tour/tip/TipPluginText.php
@@ -121,7 +121,7 @@ public function getAttributes() {
    */
   public function getOutput() {
     $output = '<h2 class="tour-tip-label" id="tour-tip-' . $this->getAriaId() . '-label">' . Html::escape($this->getLabel()) . '</h2>';
-    $output .= '<p class="tour-tip-body" id="tour-tip-' . $this->getAriaId() . '-contents">' . Xss::filterAdmin($this->token->replace($this->getBody())) . '</p>';
+    $output .= '<p class="tour-tip-body" id="tour-tip-' . $this->getAriaId() . '-contents">' . $this->token->replace($this->getBody()) . '</p>';
     return array('#markup' => $output);
   }
 
diff --git a/core/modules/user/src/Tests/UserTokenReplaceTest.php b/core/modules/user/src/Tests/UserTokenReplaceTest.php
index ad5d2966d00343cf6dafb39c36335e7042dd12eb..fd2f0329ce98bf76b623478bfcc22f3be9fefa98 100644
--- a/core/modules/user/src/Tests/UserTokenReplaceTest.php
+++ b/core/modules/user/src/Tests/UserTokenReplaceTest.php
@@ -7,7 +7,7 @@
 
 namespace Drupal\user\Tests;
 
-use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\FormattableString;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\simpletest\WebTestBase;
@@ -48,6 +48,7 @@ function testUserTokenReplacement() {
     );
 
     \Drupal::state()->set('user_hooks_test_user_format_name_alter', TRUE);
+    \Drupal::state()->set('user_hooks_test_user_format_name_alter_safe', TRUE);
 
     // Create two users and log them in one after another.
     $user1 = $this->drupalCreateUser(array());
@@ -59,21 +60,21 @@ function testUserTokenReplacement() {
     $account = User::load($user1->id());
     $global_account = User::load(\Drupal::currentUser()->id());
 
-    // Generate and test sanitized tokens.
+    // Generate and test tokens.
     $tests = array();
     $tests['[user:uid]'] = $account->id();
-    $tests['[user:name]'] = Html::escape($account->getAccountName());
-    $tests['[user:account-name]'] = Html::escape($account->getAccountName());
+    $tests['[user:name]'] = $account->getAccountName();
+    $tests['[user:account-name]'] = $account->getAccountName();
     $tests['[user:display-name]'] = $account->getDisplayName();
-    $tests['[user:mail]'] = Html::escape($account->getEmail());
+    $tests['[user:mail]'] = $account->getEmail();
     $tests['[user:url]'] = $account->url('canonical', $url_options);
     $tests['[user:edit-url]'] = $account->url('edit-form', $url_options);
     $tests['[user:last-login]'] = format_date($account->getLastLoginTime(), 'medium', '', NULL, $language_interface->getId());
     $tests['[user:last-login:short]'] = format_date($account->getLastLoginTime(), 'short', '', NULL, $language_interface->getId());
     $tests['[user:created]'] = format_date($account->getCreatedTime(), 'medium', '', NULL, $language_interface->getId());
     $tests['[user:created:short]'] = format_date($account->getCreatedTime(), 'short', '', NULL, $language_interface->getId());
-    $tests['[current-user:name]'] = Html::escape($global_account->getAccountName());
-    $tests['[current-user:account-name]'] = Html::escape($global_account->getAccountName());
+    $tests['[current-user:name]'] = $global_account->getAccountName();
+    $tests['[current-user:account-name]'] = $global_account->getAccountName();
     $tests['[current-user:display-name]'] = $global_account->getDisplayName();
 
     $base_bubbleable_metadata = BubbleableMetadata::createFromObject($account);
@@ -105,8 +106,8 @@ function testUserTokenReplacement() {
 
     foreach ($tests as $input => $expected) {
       $bubbleable_metadata = new BubbleableMetadata();
-      $output = $token_service->replace($input, array('user' => $account), array('langcode' => $language_interface->getId()), $bubbleable_metadata);
-      $this->assertEqual($output, $expected, format_string('Sanitized user token %token replaced.', array('%token' => $input)));
+      $output = $token_service->replace($input, ['user' => $account], ['langcode' => $language_interface->getId()], $bubbleable_metadata);
+      $this->assertEqual($output, $expected, new FormattableString('User token %token replaced.', ['%token' => $input]));
       $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]);
     }
 
@@ -130,21 +131,6 @@ function testUserTokenReplacement() {
       $this->assertEqual($bubbleable_metadata, $metadata_tests[$input]);
     }
 
-    // Generate and test unsanitized tokens.
-    $tests = [];
-    $tests['[user:name]'] = $account->getAccountName();
-    $tests['[user:account-name]'] = $account->getAccountName();
-    $tests['[user:display-name]'] = $account->getDisplayName();
-    $tests['[user:mail]'] = $account->getEmail();
-    $tests['[current-user:account-name]'] = $global_account->getAccountname();
-    $tests['[current-user:name]'] = $global_account->getAccountName();
-    $tests['[current-user:display-name]'] = $global_account->getDisplayName();
-
-    foreach ($tests as $input => $expected) {
-      $output = $token_service->replace($input, array('user' => $account), array('langcode' => $language_interface->getId(), 'sanitize' => FALSE));
-      $this->assertEqual($output, $expected, format_string('Unsanitized user token %token replaced.', array('%token' => $input)));
-    }
-
     // Generate login and cancel link.
     $tests = array();
     $tests['[user:one-time-login-url]'] = user_pass_reset_url($account);
@@ -153,7 +139,7 @@ function testUserTokenReplacement() {
     // Generate tokens with interface language.
     $link = \Drupal::url('user.page', [], array('absolute' => TRUE));
     foreach ($tests as $input => $expected) {
-      $output = $token_service->replace($input, array('user' => $account), array('langcode' => $language_interface->getId(), 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE));
+      $output = $token_service->replace($input, ['user' => $account], ['langcode' => $language_interface->getId(), 'callback' => 'user_mail_tokens', 'clear' => TRUE]);
       $this->assertTrue(strpos($output, $link) === 0, 'Generated URL is in interface language.');
     }
 
@@ -162,7 +148,7 @@ function testUserTokenReplacement() {
     $account->save();
     $link = \Drupal::url('user.page', [], array('language' => \Drupal::languageManager()->getLanguage($account->getPreferredLangcode()), 'absolute' => TRUE));
     foreach ($tests as $input => $expected) {
-      $output = $token_service->replace($input, array('user' => $account), array('callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE));
+      $output = $token_service->replace($input, ['user' => $account], ['callback' => 'user_mail_tokens', 'clear' => TRUE]);
       $this->assertTrue(strpos($output, $link) === 0, "Generated URL is in the user's preferred language.");
     }
 
@@ -170,9 +156,17 @@ function testUserTokenReplacement() {
     $link = \Drupal::url('user.page', [], array('language' => \Drupal::languageManager()->getLanguage('de'), 'absolute' => TRUE));
     foreach ($tests as $input => $expected) {
       foreach (array($user1, $user2) as $account) {
-        $output = $token_service->replace($input, array('user' => $account), array('langcode' => 'de', 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE));
+        $output = $token_service->replace($input, ['user' => $account], ['langcode' => 'de', 'callback' => 'user_mail_tokens', 'clear' => TRUE]);
         $this->assertTrue(strpos($output, $link) === 0, "Generated URL in in the requested language.");
       }
     }
+
+    // Generate user display name tokens when safe markup is returned.
+    // @see user_hooks_test_user_format_name_alter()
+    \Drupal::state()->set('user_hooks_test_user_format_name_alter_safe', TRUE);
+    $input = '[user:display-name] [current-user:display-name]';
+    $expected = "<em>{$user1->id()}</em> <em>{$user2->id()}</em>";
+    $output = $token_service->replace($input, ['user' => $user1]);
+    $this->assertEqual($output, $expected, new FormattableString('User token %token does not escape safe markup.', ['%token' => 'display-name']));
   }
 }
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 084b991e3e3d183b42523ba760c903801711f2d8..a7f752a880a50fc667e4042e759ab28d304c0c93 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -1,6 +1,7 @@
 <?php
 
 use Drupal\Component\Utility\Crypt;
+use Drupal\Component\Utility\PlainTextOutput;
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Asset\AttachedAssetsInterface;
 use Drupal\Core\Cache\Cache;
@@ -916,10 +917,8 @@ function user_mail($key, &$message, $params) {
   $language_manager->setConfigOverrideLanguage($language);
   $mail_config = \Drupal::config('user.mail');
 
-   // We do not sanitize the token replacement, since the output of this
-   // replacement is intended for an email message, not a web browser.
-  $token_options = array('langcode' => $langcode, 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE);
-  $message['subject'] .= $token_service->replace($mail_config->get($key . '.subject'), $variables, $token_options);
+  $token_options = ['langcode' => $langcode, 'callback' => 'user_mail_tokens', 'clear' => TRUE];
+  $message['subject'] .= PlainTextOutput::renderFromHtml($token_service->replace($mail_config->get($key . '.subject'), $variables, $token_options));
   $message['body'][] = $token_service->replace($mail_config->get($key . '.body'), $variables, $token_options);
 
   $language_manager->setConfigOverrideLanguage($original_language);
diff --git a/core/modules/user/user.tokens.inc b/core/modules/user/user.tokens.inc
index 087724e70f4999122b5a78436a7dfecda3aebfe0..7f456f66141b52d3df4866f9de28cebcf16d9c93 100644
--- a/core/modules/user/user.tokens.inc
+++ b/core/modules/user/user.tokens.inc
@@ -5,7 +5,6 @@
  * Builds placeholder replacement tokens for user-related data.
  */
 
-use Drupal\Component\Utility\Html;
 use Drupal\Core\Datetime\Entity\DateFormat;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\user\Entity\User;
@@ -85,8 +84,6 @@ function user_tokens($type, $tokens, array $data, array $options, BubbleableMeta
   else {
     $langcode = NULL;
   }
-  $sanitize = !empty($options['sanitize']);
-
   $replacements = array();
 
   if ($type == 'user' && !empty($data['user'])) {
@@ -110,14 +107,14 @@ function user_tokens($type, $tokens, array $data, array $options, BubbleableMeta
         case 'name':
         case 'account-name':
           $display_name = $account->getAccountName();
-          $replacements[$original] = $sanitize ? Html::escape($display_name) : $display_name;
+          $replacements[$original] = $display_name;
           if ($account->isAnonymous()) {
             $bubbleable_metadata->addCacheableDependency(\Drupal::config('user.settings'));
           }
           break;
 
         case 'mail':
-          $replacements[$original] = $sanitize ? Html::escape($account->getEmail()) : $account->getEmail();
+          $replacements[$original] = $account->getEmail();
           break;
 
         case 'url':
diff --git a/core/modules/views/src/Plugin/views/area/Entity.php b/core/modules/views/src/Plugin/views/area/Entity.php
index 3234f88260d334acd0a1304c20a751a8e6bb55d9..ed172329daa0cfae19e880d6330d534668506de5 100644
--- a/core/modules/views/src/Plugin/views/area/Entity.php
+++ b/core/modules/views/src/Plugin/views/area/Entity.php
@@ -162,7 +162,9 @@ public function render($empty = FALSE) {
       // @todo Use a method to check for tokens in
       //   https://www.drupal.org/node/2396607.
       if (strpos($this->options['target'], '{{') !== FALSE) {
-        $target_id = $this->tokenizeValue($this->options['target']);
+        // We cast as we need the integer/string value provided by the
+        // ::tokenizeValue() call.
+        $target_id = (string) $this->tokenizeValue($this->options['target']);
         if ($entity = $this->entityManager->getStorage($this->entityType)->load($target_id)) {
           $target_entity = $entity;
         }
diff --git a/core/modules/views/views.tokens.inc b/core/modules/views/views.tokens.inc
index 33d162bdb671ea0e7d3435b7b39da8167d378cf8..6d9de956fc316466acdad70971aa219be8358e41 100644
--- a/core/modules/views/views.tokens.inc
+++ b/core/modules/views/views.tokens.inc
@@ -5,7 +5,6 @@
  * Token integration for the views module.
  */
 
-use Drupal\Component\Utility\Html;
 use Drupal\Core\Render\BubbleableMetadata;
 
 /**
@@ -74,8 +73,6 @@ function views_tokens($type, $tokens, array $data, array $options, BubbleableMet
   if (isset($options['language'])) {
     $url_options['language'] = $options['language'];
   }
-  $sanitize = !empty($options['sanitize']);
-
   $replacements = array();
 
   if ($type == 'view' && !empty($data['view'])) {
@@ -87,11 +84,11 @@ function views_tokens($type, $tokens, array $data, array $options, BubbleableMet
     foreach ($tokens as $name => $original) {
       switch ($name) {
         case 'label':
-          $replacements[$original] = $sanitize ? Html::escape($view->storage->label()) : $view->storage->label();
+          $replacements[$original] = $view->storage->label();
           break;
 
         case 'description':
-          $replacements[$original] = $sanitize ? Html::escape($view->storage->get('description')) : $view->storage->get('description');
+          $replacements[$original] = $view->storage->get('description');
           break;
 
         case 'id':
@@ -100,7 +97,7 @@ function views_tokens($type, $tokens, array $data, array $options, BubbleableMet
 
         case 'title':
           $title = $view->getTitle();
-          $replacements[$original] = $sanitize ? Html::escape($title) : $title;
+          $replacements[$original] = $title;
           break;
 
         case 'url':
diff --git a/core/tests/Drupal/Tests/Core/Utility/TokenTest.php b/core/tests/Drupal/Tests/Core/Utility/TokenTest.php
index a738282a401f3a0b95fb742ebebd847d49f5dbda..e9021341193962488671627e4775dde0aa2b9255 100644
--- a/core/tests/Drupal/Tests/Core/Utility/TokenTest.php
+++ b/core/tests/Drupal/Tests/Core/Utility/TokenTest.php
@@ -7,10 +7,12 @@
 
 namespace Drupal\Tests\Core\Utility;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Core\Cache\Context\CacheContextsManager;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Render\SafeString;
 use Drupal\Core\Utility\Token;
 use Drupal\Tests\UnitTestCase;
 
@@ -262,4 +264,39 @@ public function testResetInfo() {
     $this->token->resetInfo();
   }
 
+  /**
+   * @covers ::replace
+   * @dataProvider providerTestReplaceEscaping
+   */
+  public function testReplaceEscaping($string, array $tokens, $expected) {
+    $this->moduleHandler->expects($this->any())
+      ->method('invokeAll')
+      ->willReturnCallback(function ($type, $args) {
+        return $args[2]['tokens'];
+      });
+
+    $result = $this->token->replace($string, ['tokens' => $tokens]);
+    $this->assertInternalType('string', $result);
+    $this->assertEquals($expected, $result);
+  }
+
+  public function providerTestReplaceEscaping() {
+    $data = [];
+
+    // No tokens. The first argument to Token::replace() should not be escaped.
+    $data['no-tokens'] = ['muh', [], 'muh'];
+    $data['html-in-string'] = ['<h1>Giraffe</h1>', [], '<h1>Giraffe</h1>'];
+    $data['html-in-string-quote'] = ['<h1>Giraffe"</h1>', [], '<h1>Giraffe"</h1>'];
+
+    $data['simple-placeholder-with-plain-text'] = ['<h1>[token:meh]</h1>', ['[token:meh]' => 'Giraffe"'], '<h1>' . Html::escape('Giraffe"') . '</h1>'];
+
+    $data['simple-placeholder-with-safe-html'] = [
+      '<h1>[token:meh]</h1>',
+      ['[token:meh]' => SafeString::create('<em>Emphasized</em>')],
+      '<h1><em>Emphasized</em></h1>',
+    ];
+
+    return $data;
+  }
+
 }