diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 009bb33..53aeea7 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1254,6 +1254,10 @@ function template_preprocess_html(&$variables) { $site_config = \Drupal::config('system.site'); // Construct page title. + if (isset($variables['page']['#title']) && is_array($variables['page']['#title'])) { + // Do an early render if the title is a render array. + $variables['page']['#title'] = (string) \Drupal::service('renderer')->render($variables['page']['#title']); + } if (!empty($variables['page']['#title'])) { $head_title = array( 'title' => trim(strip_tags($variables['page']['#title'])), diff --git a/core/lib/Drupal/Component/Utility/SafeMarkup.php b/core/lib/Drupal/Component/Utility/SafeMarkup.php index 2fb5152..91a2f1e 100644 --- a/core/lib/Drupal/Component/Utility/SafeMarkup.php +++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php @@ -15,9 +15,9 @@ * provides a store for known safe strings and methods to manage them * throughout the page request. * - * Strings sanitized by self::checkPlain() and self::escape() or - * self::xssFilter() are automatically marked safe, as are markup strings - * created from @link theme_render render arrays @endlink via drupal_render(). + * Strings sanitized by self::checkPlain() and self::escape() are automatically + * marked safe, as are markup strings created from @link theme_render render + * arrays @endlink via drupal_render(). * * This class should be limited to internal use only. Module developers should * instead use the appropriate @@ -139,60 +139,6 @@ public static function escape($string) { } /** - * Filters HTML for XSS vulnerabilities and marks the result as safe. - * - * Calling this method unnecessarily will result in bloating the safe string - * list and increases the chance of unintended side effects. - * - * If Twig receives a value that is not marked as safe then it will - * automatically encode special characters in a plain-text string for display - * as HTML. Therefore, SafeMarkup::xssFilter() should only be used when the - * string might contain HTML that needs to be rendered properly by the - * browser. - * - * If you need to filter for admin use, like Xss::filterAdmin(), then: - * - If the string is used as part of a @link theme_render render array @endlink, - * use #markup to allow the render system to filter by the admin tag list - * automatically. - * - Otherwise, use the SafeMarkup::xssFilter() with tag list provided by - * Xss::getAdminTagList() instead. - * - * This method should only be used instead of Xss::filter() when the result is - * being added to a render array that is constructed before rendering begins. - * - * In the rare instance that the caller does not want to filter strings that - * are marked safe already, it needs to check SafeMarkup::isSafe() itself. - * - * @param $string - * The string with raw HTML in it. It will be stripped of everything that - * can cause an XSS attack. The string provided will always be escaped - * regardless of whether the string is already marked as safe. - * @param array $html_tags - * (optional) An array of HTML tags. If omitted, it uses the default tag - * list defined by \Drupal\Component\Utility\Xss::filter(). - * - * @return string - * An XSS-safe version of $string, or an empty string if $string is not - * valid UTF-8. The string is marked as safe. - * - * @ingroup sanitization - * - * @see \Drupal\Component\Utility\Xss::filter() - * @see \Drupal\Component\Utility\Xss::filterAdmin() - * @see \Drupal\Component\Utility\Xss::getAdminTagList() - * @see \Drupal\Component\Utility\SafeMarkup::isSafe() - */ - public static function xssFilter($string, $html_tags = NULL) { - if (is_null($html_tags)) { - $string = Xss::filter($string); - } - else { - $string = Xss::filter($string, $html_tags); - } - return static::set($string); - } - - /** * Gets all strings currently marked as safe. * * This is useful for the batch and form APIs, where it is important to diff --git a/core/lib/Drupal/Component/Utility/Xss.php b/core/lib/Drupal/Component/Utility/Xss.php index a68ca2b..6fdeb68 100644 --- a/core/lib/Drupal/Component/Utility/Xss.php +++ b/core/lib/Drupal/Component/Utility/Xss.php @@ -15,7 +15,7 @@ class Xss { /** - * The list of html tags allowed by filterAdmin(). + * The list of HTML tags allowed by filterAdmin(). * * @var array * @@ -24,18 +24,20 @@ class Xss { protected static $adminTags = array('a', 'abbr', 'acronym', 'address', 'article', 'aside', 'b', 'bdi', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'command', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'figcaption', 'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'mark', 'menu', 'meter', 'nav', 'ol', 'output', 'p', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'small', 'span', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time', 'tr', 'tt', 'u', 'ul', 'var', 'wbr'); /** + * The default list of HTML tags allowed by filter(). + * + * @var array + * + * @see \Drupal\Component\Utility\Xss::filter() + */ + protected static $htmlTags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd'); + + /** * Filters HTML to prevent cross-site-scripting (XSS) vulnerabilities. * * Based on kses by Ulf Harnhammar, see http://sourceforge.net/projects/kses. * For examples of various XSS attacks, see: http://ha.ckers.org/xss.html. * - * This method is preferred to - * \Drupal\Component\Utility\SafeMarkup::xssFilter() when the result is not - * being used directly in the rendering system (for example, when its result - * is being combined with other strings before rendering). This avoids - * bloating the safe string list with partial strings if the whole result will - * be marked safe. - * * This code does four things: * - Removes characters and constructs that can trick browsers. * - Makes sure all HTML entities are well-formed. @@ -54,11 +56,13 @@ class Xss { * valid UTF-8. * * @see \Drupal\Component\Utility\Unicode::validateUtf8() - * @see \Drupal\Component\Utility\SafeMarkup::xssFilter() * * @ingroup sanitization */ - public static function filter($string, $html_tags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd')) { + public static function filter($string, array $html_tags = NULL) { + if (is_null($html_tags)) { + $html_tags = static::$htmlTags; + } // Only operate on valid UTF-8 strings. This is necessary to prevent cross // site scripting issues on Internet Explorer 6. if (!Unicode::validateUtf8($string)) { @@ -108,13 +112,6 @@ public static function filter($string, $html_tags = array('a', 'em', 'strong', ' * is desired (so \Drupal\Component\Utility\SafeMarkup::checkPlain() is * not acceptable). * - * This method is preferred to - * \Drupal\Component\Utility\SafeMarkup::xssFilter() when the result is - * not being used directly in the rendering system (for example, when its - * result is being combined with other strings before rendering). This avoids - * bloating the safe string list with partial strings if the whole result will - * be marked safe. - * * Allows all tags that can be used inside an HTML body, save * for scripts and styles. * @@ -126,7 +123,6 @@ public static function filter($string, $html_tags = array('a', 'em', 'strong', ' * * @ingroup sanitization * - * @see \Drupal\Component\Utility\SafeMarkup::xssFilter() * @see \Drupal\Component\Utility\Xss::getAdminTagList() * */ @@ -338,13 +334,22 @@ protected static function needsRemoval($html_tags, $elem) { } /** - * Gets the list of html tags allowed by Xss::filterAdmin(). + * Gets the list of HTML tags allowed by Xss::filterAdmin(). * * @return array - * The list of html tags allowed by filterAdmin(). + * The list of HTML tags allowed by filterAdmin(). */ public static function getAdminTagList() { return static::$adminTags; } + /** + * Gets the standard list of HTML tags allowed by Xss::filter(). + * + * @return array + * The list of HTML tags allowed by Xss::filter(). + */ + public static function getHtmlTagList() { + return static::$htmlTags; + } } diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 2ef11d1..fb5e066 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Render; +use Drupal\Component\Utility\Html; use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\UrlHelper; use Drupal\Component\Utility\Xss; @@ -415,7 +416,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) { if (!empty($elements['#markup'])) { // @todo Decide how to support non-HTML in the render API in // https://www.drupal.org/node/2501313. - $elements['#markup'] = $this->xssFilterAdminIfUnsafe($elements['#markup']); + $elements['#markup'] = $this->ensureMarkupIsSafe($elements); } // Assume that if #theme is set it represents an implemented hook. @@ -737,6 +738,54 @@ public function addCacheableDependency(array &$elements, $dependency) { } /** + * Escapes or filters #markup as required. + * + * Drupal uses Twig's auto-escape feature to improve security. This feature + * automatically escapes any HTML that is not known to be safe. Due to this + * the render system needs to ensure that all markup it generates is marked + * safe so that Twig does not do any additional escaping. + * + * By default all #markup is filtered to protect against XSS using the admin + * tag list. Render arrays can alter the list of tags allowed by the filter + * using the #markup_allowed_tags property. This value should be an array of + * tags that Xss::filter() would accept. Render arrays can escape #markup + * instead of XSS filtering by setting the #markup_safe_strategy property to + * RendererInterface::MARKUP_SAFE_STRATEGY_ESCAPE. If the escaping strategy is + * used #markup_allowed_tags is ignored. + * + * @param array $elements + * A render array with #markup set. + * + * @return \Drupal\Component\Utility\SafeStringInterface|string + * The escaped markup wrapped in a SafeString object. If + * SafeMarkup::isSafe($elements['#markup']) returns TRUE, it won't be + * escaped or filtered again. + * + * @see \Drupal\Component\Utility\Xss::filter() + * @see \Drupal\Component\Utility\Xss::adminFilter() + * @see htmlspecialchars() + * @see \Drupal\Core\Render\RendererInterface::MARKUP_SAFE_STRATEGY_FILTER + * @see \Drupal\Core\Render\RendererInterface::MARKUP_SAFE_STRATEGY_ESCAPE + */ + protected function ensureMarkupIsSafe(array $elements) { + $strategy = isset($elements['#markup_safe_strategy']) ? $elements['#markup_safe_strategy'] : static::MARKUP_SAFE_STRATEGY_FILTER; + if (SafeMarkup::isSafe($elements['#markup'])) { + // Nothing to do as #markup is already marked as safe. + return $elements['#markup']; + } + elseif ($strategy == static::MARKUP_SAFE_STRATEGY_ESCAPE) { + $markup = Html::escape($elements['#markup']); + } + else { + // The default behaviour is to XSS filter using the admin tag list. + $tags = isset($elements['#markup_allowed_tags']) ? $elements['#markup_allowed_tags'] : Xss::getAdminTagList(); + $markup = Xss::filter($elements['#markup'], $tags); + } + + return SafeString::create($markup); + } + + /** * Applies a very permissive XSS/HTML filter for admin-only use. * * Note: This method only filters if $string is not marked safe already. This diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php index 155eec4..acad41f 100644 --- a/core/lib/Drupal/Core/Render/RendererInterface.php +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -13,6 +13,22 @@ interface RendererInterface { /** + * #markup_safe_strategy indicating #markup should be filtered. + * + * @see \Drupal\Core\Render\ensureMarkupIsSafe::ensureSafeMarkup() + * @see \Drupal\Component\Utility\Xss::filter() + */ + const MARKUP_SAFE_STRATEGY_FILTER = 'xss'; + + /** + * #markup_safe_strategy indicating #markup should be escaped. + * + * @see \Drupal\Core\Render\Renderer::ensureMarkupIsSafe() + * @see htmlspecialchars() + */ + const MARKUP_SAFE_STRATEGY_ESCAPE = 'escape'; + + /** * Renders final HTML given a structured array tree. * * Calls ::render() in such a way that placeholders are replaced. diff --git a/core/lib/Drupal/Core/Render/theme.api.php b/core/lib/Drupal/Core/Render/theme.api.php index 689dce0..a7c89fd 100644 --- a/core/lib/Drupal/Core/Render/theme.api.php +++ b/core/lib/Drupal/Core/Render/theme.api.php @@ -271,10 +271,25 @@ * vectors. (I.e, test"], ], "This is alert('XSS') test"]; + // XSS filtering test. + $data[] = [[ + 'child' => ['#markup' => "This is test", '#markup_allowed_tags' => ['script']], + ], "This is test"]; + // XSS filtering test. + $data[] = [[ + 'child' => ['#markup' => "This is test", '#markup_allowed_tags' => ['em', 'strong']], + ], "This is alert('XSS') test"]; + // Html escaping test. + $data[] = [[ + 'child' => ['#markup' => "This is test", '#markup_safe_strategy' => RendererInterface::MARKUP_SAFE_STRATEGY_ESCAPE], + ], "This is <script><em>alert('XSS')</em></script> <strong>test</strong>"]; + // XSS filtering by default test. + $data[] = [[ + 'child' => ['#markup' => "This is test", '#markup_safe_strategy' => 'nonsense'], + ], "This is alert('XSS') test"]; // Ensure non-XSS tags are not filtered out. $data[] = [[ 'child' => ['#markup' => "This is test"],