diff --git a/CHANGELOG.txt b/CHANGELOG.txt index fa1fd027d2bb40ad497639f5a051cf15a1dd7b07..ddfdf2c52fbd0f53525a393356c5950441096e6f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,8 @@ +Drupal 6.29, 2013-11-20 +---------------------- +- Fixed security issues (multiple vulnerabilities), see SA-CORE-2013-003. + Drupal 6.28, 2013-01-16 ---------------------- - Fixed security issues (multiple vulnerabilities), see SA-CORE-2013-001. diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 3c53a963dae1ad2e57b95986467aba5d2e2e019b..d6b407ced26b5ab634ee049cd863b52e422b4deb 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -1334,3 +1334,111 @@ function ip_address() { return $ip_address; } + +/** + * Returns a URL-safe, base64 encoded string of highly randomized bytes (over the full 8-bit range). + * + * @param $byte_count + * The number of random bytes to fetch and base64 encode. + * + * @return string + * The base64 encoded result will have a length of up to 4 * $byte_count. + */ +function drupal_random_key($byte_count = 32) { + return drupal_base64_encode(drupal_random_bytes($byte_count)); +} + +/** + * Returns a URL-safe, base64 encoded version of the supplied string. + * + * @param $string + * The string to convert to base64. + * + * @return string + */ +function drupal_base64_encode($string) { + $data = base64_encode($string); + // Modify the output so it's safe to use in URLs. + return strtr($data, array('+' => '-', '/' => '_', '=' => '')); +} + +/** + * Returns a string of highly randomized bytes (over the full 8-bit range). + * + * This function is better than simply calling mt_rand() or any other built-in + * PHP function because it can return a long string of bytes (compared to < 4 + * bytes normally from mt_rand()) and uses the best available pseudo-random + * source. + * + * @param $count + * The number of characters (bytes) to return in the string. + */ +function drupal_random_bytes($count) { + // $random_state does not use drupal_static as it stores random bytes. + static $random_state, $bytes, $has_openssl, $has_hash; + + $missing_bytes = $count - strlen($bytes); + + if ($missing_bytes > 0) { + // PHP versions prior 5.3.4 experienced openssl_random_pseudo_bytes() + // locking on Windows and rendered it unusable. + if (!isset($has_openssl)) { + $has_openssl = version_compare(PHP_VERSION, '5.3.4', '>=') && function_exists('openssl_random_pseudo_bytes'); + } + + // openssl_random_pseudo_bytes() will find entropy in a system-dependent + // way. + if ($has_openssl) { + $bytes .= openssl_random_pseudo_bytes($missing_bytes); + } + + // Else, read directly from /dev/urandom, which is available on many *nix + // systems and is considered cryptographically secure. + elseif ($fh = @fopen('/dev/urandom', 'rb')) { + // PHP only performs buffered reads, so in reality it will always read + // at least 4096 bytes. Thus, it costs nothing extra to read and store + // that much so as to speed any additional invocations. + $bytes .= fread($fh, max(4096, $missing_bytes)); + fclose($fh); + } + + // If we couldn't get enough entropy, this simple hash-based PRNG will + // generate a good set of pseudo-random bytes on any system. + // Note that it may be important that our $random_state is passed + // through hash() prior to being rolled into $output, that the two hash() + // invocations are different, and that the extra input into the first one - + // the microtime() - is prepended rather than appended. This is to avoid + // directly leaking $random_state via the $output stream, which could + // allow for trivial prediction of further "random" numbers. + if (strlen($bytes) < $count) { + // Initialize on the first call. The contents of $_SERVER includes a mix of + // user-specific and system information that varies a little with each page. + if (!isset($random_state)) { + $random_state = print_r($_SERVER, TRUE); + if (function_exists('getmypid')) { + // Further initialize with the somewhat random PHP process ID. + $random_state .= getmypid(); + } + // hash() is only available in PHP 5.1.2+ or via PECL. + $has_hash = function_exists('hash') && in_array('sha256', hash_algos()); + $bytes = ''; + } + + if ($has_hash) { + do { + $random_state = hash('sha256', microtime() . mt_rand() . $random_state); + $bytes .= hash('sha256', mt_rand() . $random_state, TRUE); + } while (strlen($bytes) < $count); + } + else { + do { + $random_state = md5(microtime() . mt_rand() . $random_state); + $bytes .= pack("H*", md5(mt_rand() . $random_state)); + } while (strlen($bytes) < $count); + } + } + } + $output = substr($bytes, 0, $count); + $bytes = substr($bytes, $count); + return $output; +} diff --git a/includes/common.inc b/includes/common.inc index 2abf1988d1047dfa54d4eaf051151ad7a0031e24..27ed5cd540c736f975fd7c930a62be0439d22689 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -2634,7 +2634,7 @@ function drupal_urlencode($text) { */ function drupal_get_private_key() { if (!($key = variable_get('drupal_private_key', 0))) { - $key = md5(uniqid(mt_rand(), true)) . md5(uniqid(mt_rand(), true)); + $key = drupal_random_key(); variable_set('drupal_private_key', $key); } return $key; @@ -2666,7 +2666,7 @@ function drupal_get_token($value = '') { */ function drupal_valid_token($token, $value = '', $skip_anonymous = FALSE) { global $user; - return (($skip_anonymous && $user->uid == 0) || ($token == md5(session_id() . $value . variable_get('drupal_private_key', '')))); + return (($skip_anonymous && $user->uid == 0) || ($token === md5(session_id() . $value . variable_get('drupal_private_key', '')))); } /** @@ -2724,6 +2724,10 @@ function _drupal_bootstrap_full() { fix_gpc_magic(); // Load all enabled modules module_load_all(); + // Ensure mt_rand is reseeded, to prevent random values from one page load + // being exploited to predict random values in subsequent page loads. + $seed = unpack("L", drupal_random_bytes(4)); + mt_srand($seed[1]); // Let all modules take action before menu system handles the request // We do not want this while running update.php. if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') { diff --git a/includes/file.inc b/includes/file.inc index 0736f8bd4b9a1ec2b0698984c1955e3d927d5087..d0e24b2e98be36bffcdfc197507d327ea042ce80 100644 --- a/includes/file.inc +++ b/includes/file.inc @@ -134,20 +134,81 @@ function file_check_directory(&$directory, $mode = 0, $form_item = NULL) { } } - if ((file_directory_path() == $directory || file_directory_temp() == $directory) && !is_file("$directory/.htaccess")) { - $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks"; + if (file_directory_path() == $directory || file_directory_temp() == $directory) { + file_create_htaccess($directory, $form_item); + } + + return TRUE; +} + +/** + * Creates a .htaccess file in the given directory. + * + * @param $directory + * The directory. + * @param $form_item + * An optional string containing the name of a form item that any errors + * will be attached to. Useful when called from file_check_directory() to + * validate a directory path entered as a form value. An error will + * consequently prevent form submit handlers from running, and instead + * display the form along with the error messages. + * @param $force_overwrite + * Set to TRUE to attempt to overwrite the existing .htaccess file if one is + * already present. Defaults to FALSE. + */ +function file_create_htaccess($directory, $form_item = NULL, $force_overwrite = FALSE) { + if (!is_file("$directory/.htaccess") || $force_overwrite) { + $htaccess_lines = file_htaccess_lines(); if (($fp = fopen("$directory/.htaccess", 'w')) && fputs($fp, $htaccess_lines)) { fclose($fp); chmod($directory .'/.htaccess', 0664); } else { $variables = array('%directory' => $directory, '!htaccess' => '
'. nl2br(check_plain($htaccess_lines))); - form_set_error($form_item, t("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: !htaccess", $variables)); + if ($form_item) { + form_set_error($form_item, t("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: !htaccess", $variables)); + } watchdog('security', "Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: !htaccess", $variables, WATCHDOG_ERROR); } } +} - return TRUE; +/** + * Returns the standard .htaccess lines that Drupal writes to file directories. + * + * @return + * A string representing the desired contents of the .htaccess file. + * + * @see file_create_htaccess() + */ +function file_htaccess_lines() { + $lines = << + # Override the handler again if we're run later in the evaluation list. + SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003 + + +# If we know how to do it safely, disable the PHP engine entirely. + + php_flag engine off + +# PHP 4, Apache 1. + + php_flag engine off + +# PHP 4, Apache 2. + + php_flag engine off + +EOF; + + return $lines; } /** diff --git a/includes/form.inc b/includes/form.inc index 514641d3348519d4a1a484c33e58f2a61fb9e686..1b0e0573cbb93fb395cce9b90e6f89b77eb834ba 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -101,7 +101,7 @@ function drupal_get_form($form_id) { array_unshift($args_temp, $form_id); $form = call_user_func_array('drupal_retrieve_form', $args_temp); - $form_build_id = 'form-'. md5(uniqid(mt_rand(), TRUE)); + $form_build_id = 'form-'. drupal_random_key(); $form['#build_id'] = $form_build_id; drupal_prepare_form($form_id, $form, $form_state); // Store a copy of the unprocessed form for caching and indicate that it @@ -196,7 +196,7 @@ function drupal_rebuild_form($form_id, &$form_state, $args, $form_build_id = NUL if (!isset($form_build_id)) { // We need a new build_id for the new version of the form. - $form_build_id = 'form-'. md5(uniqid(mt_rand(), TRUE)); + $form_build_id = 'form-'. drupal_random_key(); } $form['#build_id'] = $form_build_id; drupal_prepare_form($form_id, $form, $form_state); @@ -590,6 +590,12 @@ function drupal_validate_form($form_id, $form, &$form_state) { if (!drupal_valid_token($form_state['values']['form_token'], $form['#token'])) { // Setting this error will cause the form to fail validation. form_set_error('form_token', t('Validation error, please try again. If this error persists, please contact the site administrator.')); + + // Stop here and don't run any further validation handlers, because they + // could invoke non-safe operations which opens the door for CSRF + // vulnerabilities. + $validated_forms[$form_id] = TRUE; + return; } } diff --git a/install.php b/install.php index 0773d335a9a4da43afdf883baf9657f6e9041509..0839d385b04c0932fbf8628accd0e8d594774cf2 100644 --- a/install.php +++ b/install.php @@ -139,6 +139,16 @@ function install_main() { // Install system.module. drupal_install_system(); + + // Ensure that all of Drupal's standard directories have appropriate + // .htaccess files. These directories will have already been created by + // this point in the installer, since Drupal creates them during the + // install_check_requirements() task. Note that we cannot create them any + // earlier than this, since the code below relies on system.module in order + // to work. + file_create_htaccess(file_directory_path()); + file_create_htaccess(file_directory_temp()); + // Save the list of other modules to install for the 'profile-install' // task. variable_set() can be used now that system.module is installed // and drupal is bootstrapped. diff --git a/modules/openid/openid.inc b/modules/openid/openid.inc index 44cdde2c36c442b945a6c5d87074f9d2edca961c..70dbee9202ea2e40f9585e2a106adc2c7f83cb04 100644 --- a/modules/openid/openid.inc +++ b/modules/openid/openid.inc @@ -361,7 +361,7 @@ function _openid_dh_rand($stop) { } do { - $bytes = "\x00". _openid_get_bytes($nbytes); + $bytes = "\x00". drupal_random_bytes($nbytes); $n = _openid_dh_binary_to_long($bytes); // Keep looping if this value is in the low duplicated range. } while (bccomp($n, $duplicate) < 0); @@ -370,23 +370,7 @@ function _openid_dh_rand($stop) { } function _openid_get_bytes($num_bytes) { - static $f = null; - $bytes = ''; - if (!isset($f)) { - $f = @fopen(OPENID_RAND_SOURCE, "r"); - } - if (!$f) { - // pseudorandom used - $bytes = ''; - for ($i = 0; $i < $num_bytes; $i += 4) { - $bytes .= pack('L', mt_rand()); - } - $bytes = substr($bytes, 0, $num_bytes); - } - else { - $bytes = fread($f, $num_bytes); - } - return $bytes; + return drupal_random_bytes($num_bytes); } function _openid_response($str = NULL) { diff --git a/modules/system/system.install b/modules/system/system.install index 2421dcb90ed8e40fb83d4034f665338418e4fca0..acacd39e97ab977611284ec47368e2ee57f6d5aa 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -122,6 +122,35 @@ function system_requirements($phase) { $requirements['settings.php']['title'] = $t('Configuration file'); } + // Test the contents of the .htaccess files. + if ($phase == 'runtime') { + // Try to write the .htaccess files first, to prevent false alarms in case + // (for example) the /tmp directory was wiped. + file_create_htaccess(file_directory_path()); + file_create_htaccess(file_directory_temp()); + $htaccess_files['files_htaccess'] = array( + 'title' => $t('Files directory'), + 'directory' => file_directory_path(), + ); + $htaccess_files['temporary_files_htaccess'] = array( + 'title' => $t('Temporary files directory'), + 'directory' => file_directory_temp(), + ); + foreach ($htaccess_files as $key => $file_info) { + // Check for the string which was added to the recommended .htaccess file + // in the latest security update. + $htaccess_file = $file_info['directory'] . '/.htaccess'; + if (!file_exists($htaccess_file) || !($contents = @file_get_contents($htaccess_file)) || strpos($contents, 'Drupal_Security_Do_Not_Remove_See_SA_2013_003') === FALSE) { + $requirements[$key] = array( + 'title' => $file_info['title'], + 'value' => $t('Not fully protected'), + 'severity' => REQUIREMENT_ERROR, + 'description' => $t('See @url for information about the recommended .htaccess file which should be added to the %directory directory to help protect against arbitrary code execution.', array('@url' => 'http://drupal.org/SA-CORE-2013-003', '%directory' => $file_info['directory'])), + ); + } + } + } + // Report cron status. if ($phase == 'runtime') { // Cron warning threshold defaults to two days. diff --git a/modules/system/system.module b/modules/system/system.module index 320f51fa1570deece1feb518c8855192ed5b1daf..09732c96844da7d3114b9ac22b9939fa87e81f8f 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '6.28'); +define('VERSION', '6.29'); /** * Core API compatibility. diff --git a/modules/user/user.module b/modules/user/user.module index 9339c4412e8530744d609e62d7a3140dcdf3495e..0dea21e53273d4b385176505909eb9ae5c6c5dd0 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -477,12 +477,15 @@ function user_password($length = 10) { // Loop the number of times specified by $length. for ($i = 0; $i < $length; $i++) { + do { + // Find a secure random number within the range needed. + $index = ord(drupal_random_bytes(1)); + } while ($index > $len); // Each iteration, pick a random character from the // allowable string and append it to the password: - $pass .= $allowable_characters[mt_rand(0, $len)]; + $pass .= $allowable_characters[$index]; } - return $pass; } diff --git a/update.php b/update.php index 984065724c9c9658289384a7b98b393388619d8a..0d62ae7d3363ee70d37fa0f0b8b424613302ea87 100644 --- a/update.php +++ b/update.php @@ -655,13 +655,13 @@ function update_check_requirements() { $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : ''; switch ($op) { case 'selection': - if (isset($_GET['token']) && $_GET['token'] == drupal_get_token('update')) { + if (isset($_GET['token']) && drupal_valid_token($_GET['token'], 'update')) { $output = update_selection_page(); break; } case 'Update': - if (isset($_GET['token']) && $_GET['token'] == drupal_get_token('update')) { + if (isset($_GET['token']) && drupal_valid_token($_GET['token'], 'update')) { update_batch(); break; }