Skip to content

Instantly share code, notes, and snippets.

@nevergone
Last active April 29, 2018 14:25
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nevergone/0dfd1d1f30b4acc984474268d98c7f04 to your computer and use it in GitHub Desktop.
Save nevergone/0dfd1d1f30b4acc984474268d98c7f04 to your computer and use it in GitHub Desktop.
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