/** * Checks that harmful protocols are stripped. */ function testBadProtocolStripping() { // Ensure that check_url() strips out harmful protocols, and encodes for // HTML. // Ensure \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols() can // be used to return a plain-text string stripped of harmful protocols. $url = 'javascript:http://www.example.com/?x=1&y=2'; $expected_plain = 'http://www.example.com/?x=1&y=2'; $expected_html = 'http://www.example.com/?x=1&y=2'; $this->assertIdentical(check_url($url), $expected_html, 'check_url() filters a URL and encodes it for HTML.'); $this->assertIdentical(UrlHelper::stripDangerousProtocols($url), $expected_plain, '\\Drupal\\Component\\Utility\\Url::stripDangerousProtocols() filters a URL and returns plain text.'); }
/** * Replaces placeholders in a string with values. * * @param string $string * A string containing placeholders. The string itself is expected to be * safe and correct HTML. Any unsafe content must be in $args and * inserted via placeholders. * @param array $args * An associative array of replacements. Each array key should be the same * as a placeholder in $string. The corresponding value should be a string * or an object that implements * \Drupal\Component\Render\MarkupInterface. The value replaces the * placeholder in $string. Sanitization and formatting will be done before * replacement. The type of sanitization and formatting depends on the first * character of the key: * - @variable: When the placeholder replacement value is: * - A string, the replaced value in the returned string will be sanitized * using \Drupal\Component\Utility\Html::escape(). * - A MarkupInterface object, the replaced value in the returned string * will not be sanitized. * - A MarkupInterface object cast to a string, the replaced value in the * returned string be forcibly sanitized using * \Drupal\Component\Utility\Html::escape(). * @code * $this->placeholderFormat('This will force HTML-escaping of the replacement value: @text', ['@text' => (string) $safe_string_interface_object)); * @endcode * Use this placeholder as the default choice for anything displayed on * the site, but not within HTML attributes, JavaScript, or CSS. Doing so * is a security risk. * - %variable: Use when the replacement value is to be wrapped in <em> * tags. * A call like: * @code * $string = "%output_text"; * $arguments = ['output_text' => 'text output here.']; * $this->placeholderFormat($string, $arguments); * @endcode * makes the following HTML code: * @code * <em class="placeholder">text output here.</em> * @endcode * As with @variable, do not use this within HTML attributes, JavaScript, * or CSS. Doing so is a security risk. * - :variable: Return value is escaped with * \Drupal\Component\Utility\Html::escape() and filtered for dangerous * protocols using UrlHelper::stripDangerousProtocols(). Use this when * using the "href" attribute, ensuring the attribute value is always * wrapped in quotes: * @code * // Secure (with quotes): * $this->placeholderFormat('<a href=":url">@variable</a>', [':url' => $url, '@variable' => $variable]); * // Insecure (without quotes): * $this->placeholderFormat('<a href=:url>@variable</a>', [':url' => $url, '@variable' => $variable]); * @endcode * When ":variable" comes from arbitrary user input, the result is secure, * but not guaranteed to be a valid URL (which means the resulting output * could fail HTML validation). To guarantee a valid URL, use * Url::fromUri($user_input)->toString() (which either throws an exception * or returns a well-formed URL) before passing the result into a * ":variable" placeholder. * * @return string * A formatted HTML string with the placeholders replaced. * * @ingroup sanitization * * @see \Drupal\Core\StringTranslation\TranslatableMarkup * @see \Drupal\Core\StringTranslation\PluralTranslatableMarkup * @see \Drupal\Component\Utility\Html::escape() * @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols() * @see \Drupal\Core\Url::fromUri() */ protected static function placeholderFormat($string, array $args) { // Transform arguments before inserting them. foreach ($args as $key => $value) { switch ($key[0]) { case '@': // Escape if the value is not an object from a class that implements // \Drupal\Component\Render\MarkupInterface, for example strings will // be escaped. // Strings that are safe within HTML fragments, but not within other // contexts, may still be an instance of // \Drupal\Component\Render\MarkupInterface, so this placeholder type // must not be used within HTML attributes, JavaScript, or CSS. $args[$key] = static::placeholderEscape($value); break; case ':': // Strip URL protocols that can be XSS vectors. $value = UrlHelper::stripDangerousProtocols($value); // Escape unconditionally, without checking whether the value is an // instance of \Drupal\Component\Render\MarkupInterface. This forces // characters that are unsafe for use in an "href" HTML attribute to // be encoded. If a caller wants to pass a value that is extracted // from HTML and therefore is already HTML encoded, it must invoke // \Drupal\Component\Render\OutputStrategyInterface::renderFromHtml() // on it prior to passing it in as a placeholder value of this type. // @todo Add some advice and stronger warnings. // https://www.drupal.org/node/2569041. $args[$key] = Html::escape($value); break; case '%': // Similarly to @, escape non-safe values. Also, add wrapping markup // in order to render as a placeholder. Not for use within attributes, // per the warning above about // \Drupal\Component\Render\MarkupInterface and also due to the // wrapping markup. $args[$key] = '<em class="placeholder">' . static::placeholderEscape($value) . '</em>'; break; default: // We do not trigger an error for placeholder that start with an // alphabetic character. if (!ctype_alpha($key[0])) { // We trigger an error as we may want to introduce new placeholders // in the future without breaking backward compatibility. trigger_error('Invalid placeholder (' . $key . ') in string: ' . $string, E_USER_ERROR); } break; } } return strtr($string, $args); }
/** * {@inheritdoc} */ public function sanitizeValue($value, $type = NULL) { switch ($type) { case 'xss': $value = Xss::filter($value); break; case 'xss_admin': $value = Xss::filterAdmin($value); break; case 'url': $value = Html::escape(UrlHelper::stripDangerousProtocols($value)); break; default: $value = Html::escape($value); break; } return ViewsRenderPipelineMarkup::create($value); }
/** * {@inheritdoc} */ public function sanitizeValue($value, $type = NULL) { switch ($type) { case 'xss': $value = Xss::filter($value); break; case 'xss_admin': $value = Xss::filterAdmin($value); break; case 'url': $value = SafeMarkup::checkPlain(UrlHelper::stripDangerousProtocols($value)); break; default: $value = SafeMarkup::checkPlain($value); break; } return $value; }
/** * {@inheritdoc} */ public function generateFromPath($path = NULL, $options = array(), $collect_cacheability_metadata = FALSE) { $generated_url = $collect_cacheability_metadata ? new GeneratedUrl() : NULL; $request = $this->requestStack->getCurrentRequest(); $current_base_path = $request->getBasePath() . '/'; $current_base_url = $request->getSchemeAndHttpHost() . $current_base_path; $current_script_path = ''; $base_path_with_script = $request->getBaseUrl(); if (!empty($base_path_with_script)) { $script_name = $request->getScriptName(); if (strpos($base_path_with_script, $script_name) !== FALSE) { $current_script_path = ltrim(substr($script_name, strlen($current_base_path)), '/') . '/'; } } // Merge in defaults. $options += array('fragment' => '', 'query' => array(), 'absolute' => FALSE, 'prefix' => ''); // A duplicate of the code from // \Drupal\Component\Utility\UrlHelper::isExternal() to avoid needing // another function call, since performance inside url() is critical. if (!isset($options['external'])) { $colonpos = strpos($path, ':'); // Avoid calling drupal_strip_dangerous_protocols() if there is any slash // (/), hash (#) or question_mark (?) before the colon (:) occurrence - // if any - as this would clearly mean it is not a URL. If the path starts // with 2 slashes then it is always considered an external URL without an // explicit protocol part. $options['external'] = strpos($path, '//') === 0 || $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && UrlHelper::stripDangerousProtocols($path) == $path; } if (isset($options['fragment']) && $options['fragment'] !== '') { $options['fragment'] = '#' . $options['fragment']; } if ($options['external']) { // Split off the fragment. if (strpos($path, '#') !== FALSE) { list($path, $old_fragment) = explode('#', $path, 2); // If $options contains no fragment, take it over from the path. if (isset($old_fragment) && !$options['fragment']) { $options['fragment'] = '#' . $old_fragment; } } // Append the query. if ($options['query']) { $path .= (strpos($path, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($options['query']); } if (isset($options['https'])) { if ($options['https'] === TRUE) { $path = str_replace('http://', 'https://', $path); } elseif ($options['https'] === FALSE) { $path = str_replace('https://', 'http://', $path); } } // Reassemble. $url = $path . $options['fragment']; return $collect_cacheability_metadata ? $generated_url->setGeneratedUrl($url) : $url; } else { $path = ltrim($this->processPath($path, $options, $generated_url), '/'); } if (!isset($options['script'])) { $options['script'] = $current_script_path; } // The base_url might be rewritten from the language rewrite in domain mode. if (!isset($options['base_url'])) { if (isset($options['https'])) { if ($options['https'] === TRUE) { $options['base_url'] = str_replace('http://', 'https://', $current_base_url); $options['absolute'] = TRUE; } elseif ($options['https'] === FALSE) { $options['base_url'] = str_replace('https://', 'http://', $current_base_url); $options['absolute'] = TRUE; } } else { $options['base_url'] = $current_base_url; } } elseif (rtrim($options['base_url'], '/') == $options['base_url']) { $options['base_url'] .= '/'; } $base = $options['absolute'] ? $options['base_url'] : $current_base_path; $prefix = empty($path) ? rtrim($options['prefix'], '/') : $options['prefix']; if ($options['absolute'] && $collect_cacheability_metadata) { $generated_url->addCacheContexts(['url.site']); } $path = str_replace('%2F', '/', rawurlencode($prefix . $path)); $query = $options['query'] ? '?' . UrlHelper::buildQuery($options['query']) : ''; $url = $base . $options['script'] . $path . $query . $options['fragment']; return $collect_cacheability_metadata ? $generated_url->setGeneratedUrl($url) : $url; }
/** * Tests dangerous url protocol filtering. * * @dataProvider providerTestStripDangerousProtocols * @covers ::setAllowedProtocols * @covers ::stripDangerousProtocols * * @param string $uri * Protocol URI. * @param string $expected * Expected escaped value. * @param array $protocols * Protocols to allow. */ public function testStripDangerousProtocols($uri, $expected, $protocols) { UrlHelper::setAllowedProtocols($protocols); $stripped = UrlHelper::stripDangerousProtocols($uri); $this->assertEquals($expected, $stripped); }
/** * {@inheritdoc} */ public function generateFromPath($path = NULL, $options = array()) { $request = $this->requestStack->getCurrentRequest(); $current_base_path = $request->getBasePath() . '/'; $current_base_url = $request->getSchemeAndHttpHost() . $current_base_path; $current_script_path = ''; $base_path_with_script = $request->getBaseUrl(); if (!empty($base_path_with_script)) { $script_name = $request->getScriptName(); if (strpos($base_path_with_script, $script_name) !== FALSE) { $current_script_path = ltrim(substr($script_name, strlen($current_base_path)), '/') . '/'; } } // Merge in defaults. $options += array('fragment' => '', 'query' => array(), 'absolute' => FALSE, 'prefix' => ''); if (!isset($options['external'])) { // Return an external link if $path contains an allowed absolute URL. Only // call the slow // \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols() if $path // contains a ':' before any / ? or #. Note: we could use // \Drupal\Component\Utility\UrlHelper::isExternal($path) here, but that // would require another function call, and performance inside _url() is // critical. $colonpos = strpos($path, ':'); $options['external'] = $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && UrlHelper::stripDangerousProtocols($path) == $path; } if (isset($options['fragment']) && $options['fragment'] !== '') { $options['fragment'] = '#' . $options['fragment']; } if ($options['external']) { // Split off the fragment. if (strpos($path, '#') !== FALSE) { list($path, $old_fragment) = explode('#', $path, 2); // If $options contains no fragment, take it over from the path. if (isset($old_fragment) && !$options['fragment']) { $options['fragment'] = '#' . $old_fragment; } } // Append the query. if ($options['query']) { $path .= (strpos($path, '?') !== FALSE ? '&' : '?') . UrlHelper::buildQuery($options['query']); } if (isset($options['https']) && $this->mixedModeSessions) { if ($options['https'] === TRUE) { $path = str_replace('http://', 'https://', $path); } elseif ($options['https'] === FALSE) { $path = str_replace('https://', 'http://', $path); } } // Reassemble. return $path . $options['fragment']; } else { $path = ltrim($this->processPath($path, $options), '/'); } if (!isset($options['script'])) { $options['script'] = $current_script_path; } // The base_url might be rewritten from the language rewrite in domain mode. if (!isset($options['base_url'])) { if (isset($options['https']) && $this->mixedModeSessions) { if ($options['https'] === TRUE) { $options['base_url'] = str_replace('http://', 'https://', $current_base_url); $options['absolute'] = TRUE; } elseif ($options['https'] === FALSE) { $options['base_url'] = str_replace('https://', 'http://', $current_base_url); $options['absolute'] = TRUE; } } else { $options['base_url'] = $current_base_url; } } elseif (rtrim($options['base_url'], '/') == $options['base_url']) { $options['base_url'] .= '/'; } $base = $options['absolute'] ? $options['base_url'] : $current_base_path; $prefix = empty($path) ? rtrim($options['prefix'], '/') : $options['prefix']; $path = str_replace('%2F', '/', rawurlencode($prefix . $path)); $query = $options['query'] ? '?' . UrlHelper::buildQuery($options['query']) : ''; return $base . $options['script'] . $path . $query . $options['fragment']; }
/** * Replaces placeholders in a string with values. * * @param string $string * A string containing placeholders. The string itself is expected to be * safe and correct HTML. Any unsafe content must be in $args and * inserted via placeholders. * @param array $args * An associative array of replacements. Each array key should be the same * as a placeholder in $string. The corresponding value should be a string * or an object that implements * \Drupal\Component\Render\MarkupInterface. The value replaces the * placeholder in $string. Sanitization and formatting will be done before * replacement. The type of sanitization and formatting depends on the first * character of the key: * - @variable: When the placeholder replacement value is: * - A string, the replaced value in the returned string will be sanitized * using \Drupal\Component\Utility\Html::escape(). * - A MarkupInterface object, the replaced value in the returned string * will not be sanitized. * - A MarkupInterface object cast to a string, the replaced value in the * returned string be forcibly sanitized using * \Drupal\Component\Utility\Html::escape(). * @code * $this->placeholderFormat('This will force HTML-escaping of the replacement value: @text', ['@text' => (string) $safe_string_interface_object)); * @endcode * Use this placeholder as the default choice for anything displayed on * the site, but not within HTML attributes, JavaScript, or CSS. Doing so * is a security risk. * - %variable: Use when the replacement value is to be wrapped in <em> * tags. * A call like: * @code * $string = "%output_text"; * $arguments = ['output_text' => 'text output here.']; * $this->placeholderFormat($string, $arguments); * @endcode * makes the following HTML code: * @code * <em class="placeholder">text output here.</em> * @endcode * As with @variable, do not use this within HTML attributes, JavaScript, * or CSS. Doing so is a security risk. * - :variable: Return value is escaped with * \Drupal\Component\Utility\Html::escape() and filtered for dangerous * protocols using UrlHelper::stripDangerousProtocols(). Use this when * using the "href" attribute, ensuring the attribute value is always * wrapped in quotes: * @code * // Secure (with quotes): * $this->placeholderFormat('<a href=":url">@variable</a>', [':url' => $url, @variable => $variable]); * // Insecure (without quotes): * $this->placeholderFormat('<a href=:url>@variable</a>', [':url' => $url, @variable => $variable]); * @endcode * When ":variable" comes from arbitrary user input, the result is secure, * but not guaranteed to be a valid URL (which means the resulting output * could fail HTML validation). To guarantee a valid URL, use * Url::fromUri($user_input)->toString() (which either throws an exception * or returns a well-formed URL) before passing the result into a * ":variable" placeholder. * * @return string * A formatted HTML string with the placeholders replaced. * * @ingroup sanitization * * @see \Drupal\Core\StringTranslation\TranslatableMarkup * @see \Drupal\Core\StringTranslation\PluralTranslatableMarkup * @see \Drupal\Component\Utility\Html::escape() * @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols() * @see \Drupal\Core\Url::fromUri() */ protected static function placeholderFormat($string, array $args) { // Transform arguments before inserting them. foreach ($args as $key => $value) { switch ($key[0]) { case '@': // Escape if the value is not an object from a class that implements // \Drupal\Component\Render\MarkupInterface, for example strings will // be escaped. // \Drupal\Component\Utility\SafeMarkup\SafeMarkup::isSafe() may // return TRUE for content that is safe within HTML fragments, but not // within other contexts, so this placeholder type must not be used // within HTML attributes, JavaScript, or CSS. $args[$key] = static::placeholderEscape($value); break; case ':': // Strip URL protocols that can be XSS vectors. $value = UrlHelper::stripDangerousProtocols($value); // Escape unconditionally, without checking // \Drupal\Component\Utility\SafeMarkup\SafeMarkup::isSafe(). This // forces characters that are unsafe for use in an "href" HTML // attribute to be encoded. If a caller wants to pass a value that is // extracted from HTML and therefore is already HTML encoded, it must // invoke // \Drupal\Component\Render\OutputStrategyInterface::renderFromHtml() // on it prior to passing it in as a placeholder value of this type. // @todo Add some advice and stronger warnings. // https://www.drupal.org/node/2569041. $args[$key] = Html::escape($value); break; case '%': default: // Similarly to @, escape non-safe values. Also, add wrapping markup // in order to render as a placeholder. Not for use within attributes, // per the warning above about // \Drupal\Component\Utility\SafeMarkup\SafeMarkup::isSafe() and also // due to the wrapping markup. $args[$key] = '<em class="placeholder">' . static::placeholderEscape($value) . '</em>'; break; } } return strtr($string, $args); }