diff --git a/core/modules/filter/src/Element/ProcessedText.php b/core/modules/filter/src/Element/ProcessedText.php
index 2f92b22..9822097 100644
--- a/core/modules/filter/src/Element/ProcessedText.php
+++ b/core/modules/filter/src/Element/ProcessedText.php
@@ -8,13 +8,13 @@
 namespace Drupal\filter\Element;
 
 use Drupal\Component\Utility\NestedArray;
-use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Render\Element\RenderElement;
 use Drupal\Core\Render\Renderer;
 use Drupal\filter\Entity\FilterFormat;
 use Drupal\filter\Plugin\FilterInterface;
+use Drupal\filter\Render\FilteredString;
 
 /**
  * Provides a processed text render element.
@@ -124,7 +124,7 @@ public static function preRenderText($element) {
     // safe, but it has been passed through the filter system and checked with
     // a text format, so it must be printed as is. (See the note about security
     // in the method documentation above.)
-    $element['#markup'] = SafeMarkup::set($text);
+    $element['#markup'] = FilteredString($text);
 
     // Set the updated bubbleable rendering metadata and the text format's
     // cache tag.
diff --git a/core/modules/filter/src/Plugin/Filter/FilterCaption.php b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
index ed241d8..14f3873 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterCaption.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
@@ -13,6 +13,7 @@
 use Drupal\Component\Utility\Xss;
 use Drupal\filter\FilterProcessResult;
 use Drupal\filter\Plugin\FilterBase;
+use Drupal\filter\Render\FilteredString;
 
 /**
  * Provides a filter to caption elements.
@@ -59,7 +60,10 @@ public function process($text, $langcode) {
         $node->removeAttribute('class');
         $filter_caption = array(
           '#theme' => 'filter_caption',
-          '#node' => SafeMarkup::set($node->C14N()),
+          // We pass the unsanitized string because this is a text format
+          // filter, and after filtering, we always assume the output is safe.
+          // @see \Drupal\filter\Element\ProcessedText::preRenderText()
+          '#node' => FilteredString::create($node->C14N()),
           '#tag' => $node->tagName,
           '#caption' => $caption,
           '#classes' => $classes,
diff --git a/core/modules/filter/src/Render/FilteredString.php b/core/modules/filter/src/Render/FilteredString.php
new file mode 100644
index 0000000..2c694e6
--- /dev/null
+++ b/core/modules/filter/src/Render/FilteredString.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\filter\Render\FilteredString.
+ */
+
+namespace Drupal\filter\Render;
+
+use Drupal\Component\Utility\SafeStringInterface;
+use Drupal\Component\Utility\SafeStringTrait;
+
+/**
+ * Defines an object that passes safe strings through the Filter system.
+ *
+ * This object should only be constructed with a known safe string. If there is
+ * any risk that the string contains user-entered data that has not been
+ * filtered first, it must not be used.
+ *
+ * @internal
+ *   This object is marked as internal because it should only be used in the
+ *   Filter module on strings that have already been been filtered and sanitized
+ *   in \Drupal\filter\Plugin\FilterInterface.
+ *
+ * @see \Drupal\Core\Render\SafeString
+ */
+final class FilteredString implements SafeStringInterface, \Countable {
+  use SafeStringTrait;
+}
