D5-SA-CORE-2018-004.patch (SA-CORE-2018-002 fix included)
diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc | |
index 5b2e5ab..3d93104 100644 | |
--- a/includes/bootstrap.inc | |
+++ b/includes/bootstrap.inc | |
@@ -911,6 +911,7 @@ function _drupal_bootstrap($phase) { | |
drupal_unset_globals(); | |
// Initialize the configuration | |
conf_init(); | |
+ _drupal_bootstrap_sanitize_request(); | |
break; | |
case DRUPAL_BOOTSTRAP_EARLY_PAGE_CACHE: | |
@@ -942,6 +943,36 @@ function _drupal_bootstrap($phase) { | |
// Initialize configuration variables, using values from settings.php if available. | |
$conf = variable_init(isset($conf) ? $conf : array()); | |
+ // Sanitize the destination parameter (which is often used for redirects) | |
+ // to prevent open redirect attacks leading to other domains. Sanitize | |
+ // both $_GET['destination'] and $_REQUEST['destination'] to protect code | |
+ // that relies on either, but do not sanitize $_POST to avoid interfering | |
+ // with unrelated form submissions. $_REQUEST['edit']['destination'] is | |
+ // also sanitized since drupal_goto() will sometimes rely on it, and | |
+ // other code might therefore use it too. The sanitization happens here | |
+ // because menu_path_is_external() requires the variable system to be | |
+ // available. | |
+ if (isset($_GET['destination']) || isset($_REQUEST['destination']) || isset($_REQUEST['edit']['destination'])) { | |
+ // If the destination is an external URL, remove it. | |
+ if (isset($_GET['destination']) && _menu_path_is_external($_GET['destination'])) { | |
+ unset($_GET['destination']); | |
+ unset($_REQUEST['destination']); | |
+ } | |
+ // Ensure that the destination's query parameters are not dangerous. | |
+ if (isset($_GET['destination'])) { | |
+ _drupal_bootstrap_clean_destination(); | |
+ } | |
+ // If there's still something in $_REQUEST['destination'] that didn't | |
+ // come from $_GET, check it too. | |
+ if (isset($_REQUEST['destination']) && (!isset($_GET['destination']) || $_REQUEST['destination'] != $_GET['destination']) && _menu_path_is_external($_REQUEST['destination'])) { | |
+ unset($_REQUEST['destination']); | |
+ } | |
+ // Check $_REQUEST['edit']['destination'] separately. | |
+ if (isset($_REQUEST['edit']['destination']) && _menu_path_is_external($_REQUEST['edit']['destination'])) { | |
+ unset($_REQUEST['edit']['destination']); | |
+ } | |
+ } | |
+ | |
_drupal_cache_init($phase); | |
// Start a page timer: | |
@@ -1024,3 +1055,166 @@ function get_t() { | |
} | |
return $t; | |
} | |
+ | |
+/** | |
+ * Sanitizes unsafe keys from the request. | |
+ */ | |
+function _drupal_bootstrap_sanitize_request() { | |
+ global $conf; | |
+ static $sanitized; | |
+ | |
+ if (!$sanitized) { | |
+ // Ensure the whitelist array exists. | |
+ if (!isset($conf['sanitize_input_whitelist']) || !is_array($conf['sanitize_input_whitelist'])) { | |
+ $conf['sanitize_input_whitelist'] = array(); | |
+ } | |
+ | |
+ $sanitized_keys = _drupal_bootstrap_sanitize_input($_GET, $conf['sanitize_input_whitelist']); | |
+ $sanitized_keys = array_merge($sanitized_keys, _drupal_bootstrap_sanitize_input($_POST, $conf['sanitize_input_whitelist'])); | |
+ $sanitized_keys = array_merge($sanitized_keys, _drupal_bootstrap_sanitize_input($_REQUEST, $conf['sanitize_input_whitelist'])); | |
+ $sanitized_keys = array_merge($sanitized_keys, _drupal_bootstrap_sanitize_input($_COOKIE, $conf['sanitize_input_whitelist'])); | |
+ $sanitized_keys = array_unique($sanitized_keys); | |
+ | |
+ if (count($sanitized_keys) && !empty($conf['sanitize_input_logging'])) { | |
+ trigger_error(check_plain(sprintf('Potentially unsafe keys removed from request parameters: %s', implode(', ', $sanitized_keys)), E_USER_WARNING)); | |
+ } | |
+ | |
+ $sanitized = TRUE; | |
+ } | |
+} | |
+ | |
+/** | |
+ * Sanitizes unsafe keys from user input. | |
+ * | |
+ * @param mixed $input | |
+ * Input to sanitize. | |
+ * @param array $whitelist | |
+ * Whitelist of values. | |
+ * @return array | |
+ */ | |
+function _drupal_bootstrap_sanitize_input(&$input, $whitelist = array()) { | |
+ $sanitized_keys = array(); | |
+ | |
+ if (is_array($input)) { | |
+ foreach ($input as $key => $value) { | |
+ if ($key !== '' && $key[0] === '#' && !in_array($key, $whitelist, TRUE)) { | |
+ unset($input[$key]); | |
+ $sanitized_keys[] = $key; | |
+ } | |
+ elseif (is_array($input[$key])) { | |
+ $sanitized_keys = array_merge($sanitized_keys, _drupal_bootstrap_sanitize_input($input[$key], $whitelist)); | |
+ } | |
+ } | |
+ } | |
+ | |
+ return $sanitized_keys; | |
+} | |
+ | |
+/** | |
+ * Removes the destination if it is dangerous. | |
+ * | |
+ * Note this can only be called after common.inc has been included. | |
+ * | |
+ * @return bool | |
+ * TRUE if the destination has been removed from $_GET, FALSE if not. | |
+ */ | |
+function _drupal_bootstrap_clean_destination() { | |
+ $dangerous_keys = array(); | |
+ | |
+ $log_sanitized_keys = variable_get('sanitize_input_logging', FALSE); | |
+ | |
+ $parts = _drupal_parse_url($_GET['destination']); | |
+ if (!empty($parts['query'])) { | |
+ $whitelist = variable_get('sanitize_input_whitelist', array()); | |
+ $log_sanitized_keys = variable_get('sanitize_input_logging', FALSE); | |
+ | |
+ $dangerous_keys = _drupal_bootstrap_sanitize_input($parts['query'], $whitelist); | |
+ if (!empty($dangerous_keys)) { | |
+ // The destination is removed rather than sanitized to mirror the | |
+ // handling of external destinations. | |
+ unset($_GET['destination']); | |
+ unset($_REQUEST['destination']); | |
+ if ($log_sanitized_keys) { | |
+ trigger_error(sprintf('Potentially unsafe destination removed from query string parameters (GET) because it contained the following keys: %s', implode(', ', $dangerous_keys))); | |
+ } | |
+ return TRUE; | |
+ } | |
+ } | |
+ return FALSE; | |
+} | |
+ | |
+/** | |
+ * Backport of drupal_parse_url() from Drupal 7. | |
+ */ | |
+function _drupal_parse_url($url) { | |
+ $options = array( | |
+ 'path' => NULL, | |
+ 'query' => array(), | |
+ 'fragment' => '', | |
+ ); | |
+ | |
+ // External URLs: not using parse_url() here, so we do not have to rebuild | |
+ // the scheme, host, and path without having any use for it. | |
+ if (strpos($url, '://') !== FALSE) { | |
+ | |
+ // Split off everything before the query string into 'path'. | |
+ $parts = explode('?', $url); | |
+ $options['path'] = $parts[0]; | |
+ | |
+ // If there is a query string, transform it into keyed query parameters. | |
+ if (isset($parts[1])) { | |
+ $query_parts = explode('#', $parts[1]); | |
+ parse_str($query_parts[0], $options['query']); | |
+ | |
+ // Take over the fragment, if there is any. | |
+ if (isset($query_parts[1])) { | |
+ $options['fragment'] = $query_parts[1]; | |
+ } | |
+ } | |
+ } | |
+ else { | |
+ | |
+ // parse_url() does not support relative URLs, so make it absolute. E.g. the | |
+ // relative URL "foo/bar:1" isn't properly parsed. | |
+ $parts = parse_url('http://example.com/' . $url); | |
+ | |
+ // Strip the leading slash that was just added. | |
+ $options['path'] = substr($parts['path'], 1); | |
+ if (isset($parts['query'])) { | |
+ parse_str($parts['query'], $options['query']); | |
+ } | |
+ if (isset($parts['fragment'])) { | |
+ $options['fragment'] = $parts['fragment']; | |
+ } | |
+ } | |
+ | |
+ // The 'q' parameter contains the path of the current page if clean URLs are | |
+ // disabled. It overrides the 'path' of the URL when present, even if clean | |
+ // URLs are enabled, due to how Apache rewriting rules work. The path | |
+ // parameter must be a string. | |
+ if (isset($options['query']['q']) && is_string($options['query']['q'])) { | |
+ $options['path'] = $options['query']['q']; | |
+ unset($options['query']['q']); | |
+ } | |
+ return $options; | |
+} | |
+ | |
+/** | |
+ * Backport of menu_path_is_external() from Drupal 6. | |
+ */ | |
+function _menu_path_is_external($path) { | |
+ // Avoid calling filter_xss_bad_protocol() 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. Leading control characters may be ignored or mishandled by browsers, | |
+ // so assume such a path may lead to an external location. The range matches | |
+ // all UTF-8 control characters, class Cc. | |
+ $colonpos = strpos($path, ':'); | |
+ // Some browsers treat \ as / so normalize to forward slashes. | |
+ $path = str_replace('\\', '/', $path); | |
+ return (strpos($path, '//') === 0) || (preg_match('/^[\x00-\x1F\x7F-\x9F]/u', $path) !== 0) | |
+ || ($colonpos !== FALSE | |
+ && !preg_match('![/?#]!', substr($path, 0, $colonpos)) | |
+ && filter_xss_bad_protocol($path, FALSE) == check_plain($path)); | |
+} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment