diff options
Diffstat (limited to 'includes')
-rw-r--r-- | includes/bootstrap.inc | 73 | ||||
-rw-r--r-- | includes/common.inc | 48 | ||||
-rw-r--r-- | includes/form.inc | 11 | ||||
-rw-r--r-- | includes/menu.inc | 16 | ||||
-rw-r--r-- | includes/xmlrpcs.inc | 8 |
5 files changed, 115 insertions, 41 deletions
diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index aea944dd039a..23179ca98d35 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -1196,8 +1196,6 @@ function _drupal_bootstrap($phase) { // because menu_path_is_external() requires the variable system to be // available. if (isset($_GET['destination']) || isset($_REQUEST['destination']) || isset($_REQUEST['edit']['destination'])) { - require_once './includes/menu.inc'; - drupal_load('module', 'filter'); // If the destination is an external URL, remove it. if (isset($_GET['destination']) && menu_path_is_external($_GET['destination'])) { unset($_GET['destination']); @@ -1534,3 +1532,74 @@ function drupal_hmac_base64($data, $key) { // Modify the hmac so it's safe to use in URLs. return strtr($hmac, array('+' => '-', '/' => '_', '=' => '')); } + +/** + * Returns TRUE if a path is external (e.g. http://example.com). + * + * May be used early in bootstrap. + */ +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)); +} + +/** + * Processes an HTML attribute value and ensures it does not contain an URL + * with a disallowed protocol (e.g. javascript:) + * + * May be used early in bootstrap. + * + * @param $string + * The string with the attribute value. + * @param $decode + * Whether to decode entities in the $string. Set to FALSE if the $string + * is in plain text, TRUE otherwise. Defaults to TRUE. + * @return + * Cleaned up and HTML-escaped version of $string. + */ +function filter_xss_bad_protocol($string, $decode = TRUE) { + static $allowed_protocols; + if (!isset($allowed_protocols)) { + $allowed_protocols = array_flip(variable_get('filter_allowed_protocols', array('http', 'https', 'ftp', 'news', 'nntp', 'tel', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal', 'rtsp'))); + } + + // Get the plain text representation of the attribute value (i.e. its meaning). + if ($decode) { + $string = decode_entities($string); + } + + // Iteratively remove any invalid protocol found. + + do { + $before = $string; + $colonpos = strpos($string, ':'); + if ($colonpos > 0) { + // We found a colon, possibly a protocol. Verify. + $protocol = substr($string, 0, $colonpos); + // If a colon is preceded by a slash, question mark or hash, it cannot + // possibly be part of the URL scheme. This must be a relative URL, + // which inherits the (safe) protocol of the base document. + if (preg_match('![/?#]!', $protocol)) { + break; + } + // Per RFC2616, section 3.2.3 (URI Comparison) scheme comparison must be case-insensitive + // Check if this is a disallowed protocol. + if (!isset($allowed_protocols[strtolower($protocol)])) { + $string = substr($string, $colonpos + 1); + } + } + } while ($before != $string); + return check_plain($string); +} diff --git a/includes/common.inc b/includes/common.inc index 19798bae1c4e..9a28c06cc092 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -142,6 +142,10 @@ function drupal_clear_path_cache() { * * Note: When sending a Content-Type header, always include a 'charset' type, * too. This is necessary to avoid security bugs (e.g. UTF-7 XSS). + * + * Note: No special sanitizing needs to be done to headers. However if a header + * value contains a line break a PHP warning will be thrown and the header + * will not be set. */ function drupal_set_header($header = NULL) { // We use an array to guarantee there are no leading or trailing delimiters. @@ -150,8 +154,15 @@ function drupal_set_header($header = NULL) { static $stored_headers = array(); if (strlen($header)) { - header($header); - $stored_headers[] = $header; + // Protect against header injection attacks if PHP is too old to do that. + if (version_compare(PHP_VERSION, '5.1.2', '<') && (strpos($header, "\n") !== FALSE || strpos($header, "\r") !== FALSE)) { + // Use the same warning message that newer versions of PHP use. + trigger_error('Header may not contain more than a single header, new line detected', E_USER_WARNING); + } + else { + header($header); + $stored_headers[] = $header; + } } return implode("\n", $stored_headers); } @@ -330,14 +341,20 @@ function drupal_goto($path = '', $query = NULL, $fragment = NULL, $http_response if ($destination) { // Do not redirect to an absolute URL originating from user input. - $colonpos = strpos($destination, ':'); - $absolute = strpos($destination, '//') === 0 || ($colonpos !== FALSE && !preg_match('![/?#]!', substr($destination, 0, $colonpos))); - if (!$absolute) { - extract(parse_url(urldecode($destination))); + if (!menu_path_is_external($destination)) { + extract(parse_url($destination)); } } - $url = url($path, array('query' => $query, 'fragment' => $fragment, 'absolute' => TRUE)); + $options = array('query' => $query, 'fragment' => $fragment, 'absolute' => TRUE); + // In some cases modules call drupal_goto($_GET['q']). We need to ensure that + // such a redirect is not to an external URL. + if ($path === $_GET['q'] && menu_path_is_external($path)) { + // Force url() to generate a non-external URL. + $options['external'] = FALSE; + } + + $url = url($path, $options); // Remove newlines from the URL to avoid header injection attacks. $url = str_replace(array("\n", "\r"), '', $url); @@ -672,7 +689,7 @@ function drupal_error_handler($errno, $message, $filename, $line, $context) { return; } - if ($errno & (E_ALL ^ E_DEPRECATED)) { + if ($errno & (E_ALL ^ E_DEPRECATED ^ E_NOTICE)) { $types = array(1 => 'error', 2 => 'warning', 4 => 'parse error', 8 => 'notice', 16 => 'core error', 32 => 'core warning', 64 => 'compile error', 128 => 'compile warning', 256 => 'user error', 512 => 'user warning', 1024 => 'user notice', 2048 => 'strict warning', 4096 => 'recoverable fatal error'); // For database errors, we want the line number/file name of the place that @@ -1480,20 +1497,9 @@ function url($path = NULL, $options = array()) { 'alias' => FALSE, 'prefix' => '' ); - // A duplicate of the code from menu_path_is_external() to avoid needing - // another function call, since performance inside url() is critical. + if (!isset($options['external'])) { - // Return an external link if $path contains an allowed absolute URL. 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. - $colonpos = strpos($path, ':'); - $options['external'] = (strpos($path, '//') === 0) - || ($colonpos !== FALSE - && !preg_match('![/?#]!', substr($path, 0, $colonpos)) - && filter_xss_bad_protocol($path, FALSE) == check_plain($path)); + $options['external'] = menu_path_is_external($path); } // May need language dependent rewriting if language.inc is present. diff --git a/includes/form.inc b/includes/form.inc index b55d724a8d7f..1f485ef1bcc9 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -1066,9 +1066,16 @@ function _form_builder_handle_input_element($form_id, &$form, &$form_state, $com $form['#attributes']['disabled'] = 'disabled'; } + // With JavaScript or other easy hacking, input can be submitted even for + // elements with #access=FALSE. For security, these must not be processed. + // For pages with multiple forms, ensure that input is only processed for the + // submitted form. drupal_execute() may bypass these checks and be treated as + // a high privilege user submitting a single form. + $process_input = $form['#programmed'] || ((!isset($form['#access']) || $form['#access']) && isset($form['#post']) && (isset($form['#post']['form_id']) && $form['#post']['form_id'] == $form_id)); + if (!isset($form['#value']) && !array_key_exists('#value', $form)) { $function = !empty($form['#value_callback']) ? $form['#value_callback'] : 'form_type_'. $form['#type'] .'_value'; - if (($form['#programmed']) || ((!isset($form['#access']) || $form['#access']) && isset($form['#post']) && (isset($form['#post']['form_id']) && $form['#post']['form_id'] == $form_id))) { + if ($process_input) { $edit = $form['#post']; foreach ($form['#parents'] as $parent) { $edit = isset($edit[$parent]) ? $edit[$parent] : NULL; @@ -1113,7 +1120,7 @@ function _form_builder_handle_input_element($form_id, &$form, &$form_state, $com // We compare the incoming values with the buttons defined in the form, // and flag the one that matches. We have to do some funky tricks to // deal with Internet Explorer's handling of single-button forms, though. - if (!empty($form['#post']) && isset($form['#executes_submit_callback'])) { + if ($process_input && !empty($form['#post']) && isset($form['#executes_submit_callback'])) { // First, accumulate a collection of buttons, divided into two bins: // those that execute full submit callbacks and those that only validate. $button_type = $form['#executes_submit_callback'] ? 'submit' : 'button'; diff --git a/includes/menu.inc b/includes/menu.inc index ef27d91e7791..4788855415d1 100644 --- a/includes/menu.inc +++ b/includes/menu.inc @@ -2470,22 +2470,6 @@ function _menu_router_build($callbacks) { } /** - * Returns TRUE if a path is external (e.g. http://example.com). - */ -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. - $colonpos = strpos($path, ':'); - return (strpos($path, '//') === 0) - || ($colonpos !== FALSE - && !preg_match('![/?#]!', substr($path, 0, $colonpos)) - && filter_xss_bad_protocol($path, FALSE) == check_plain($path)); -} - -/** * Checks whether the site is off-line for maintenance. * * This function will log the current user out and redirect to front page diff --git a/includes/xmlrpcs.inc b/includes/xmlrpcs.inc index 049a92b86738..18e5ac70e661 100644 --- a/includes/xmlrpcs.inc +++ b/includes/xmlrpcs.inc @@ -213,6 +213,10 @@ function xmlrpc_server_call($xmlrpc_server, $methodname, $args) { function xmlrpc_server_multicall($methodcalls) { // See http://www.xmlrpc.com/discuss/msgReader$1208 + // To avoid multicall expansion attacks, limit the number of duplicate method + // calls allowed with a default of 1. Set to -1 for unlimited. + $duplicate_method_limit = variable_get('xmlrpc_multicall_duplicate_method_limit', 1); + $method_count = array(); $return = array(); $xmlrpc_server = xmlrpc_server_get(); foreach ($methodcalls as $call) { @@ -222,10 +226,14 @@ function xmlrpc_server_multicall($methodcalls) { $ok = FALSE; } $method = $call['methodName']; + $method_count[$method] = isset($method_count[$method]) ? $method_count[$method] + 1 : 1; $params = $call['params']; if ($method == 'system.multicall') { $result = xmlrpc_error(-32600, t('Recursive calls to system.multicall are forbidden.')); } + elseif ($duplicate_method_limit > 0 && $method_count[$method] > $duplicate_method_limit) { + $result = xmlrpc_error(-156579, t('Too many duplicate method calls in system.multicall.')); + } elseif ($ok) { $result = xmlrpc_server_call($xmlrpc_server, $method, $params); } |