diff options
-rw-r--r-- | includes/bootstrap.inc | 46 | ||||
-rw-r--r-- | includes/common.inc | 6 | ||||
-rw-r--r-- | includes/database/database.inc | 7 | ||||
-rw-r--r-- | install.php | 2 | ||||
-rw-r--r-- | modules/simpletest/drupal_web_test_case.php | 9 | ||||
-rw-r--r-- | modules/simpletest/simpletest.install | 9 |
6 files changed, 69 insertions, 10 deletions
diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index ba88daf7b..7da9cdd83 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -1343,6 +1343,13 @@ function _drupal_bootstrap($phase) { break; case DRUPAL_BOOTSTRAP_DATABASE: + // The user agent header is used to pass a database prefix in the request when + // running tests. However, for security reasons, it is imperative that we + // validate we ourselves made the request. + if (isset($_SERVER['HTTP_USER_AGENT']) && (strpos($_SERVER['HTTP_USER_AGENT'], "simpletest") !== FALSE) && !drupal_valid_test_ua($_SERVER['HTTP_USER_AGENT'])) { + header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); + exit; + } // Initialize the database system. Note that the connection // won't be initialized until it is actually requested. require_once DRUPAL_ROOT . '/includes/database/database.inc'; @@ -1429,6 +1436,45 @@ function _drupal_bootstrap($phase) { } /** + * Validate the HMAC and timestamp of a user agent header from simpletest. + */ +function drupal_valid_test_ua($user_agent) { + global $databases; + + list($prefix, $time, $salt, $hmac) = explode(';', $user_agent); + $check_string = $prefix . ';' . $time . ';' . $salt; + // We use the database credentials from settings.php to make the HMAC key, since + // the database is not yet initialized and we can't access any Drupal variables. + // The file properties add more entropy not easily accessible to others. + $filepath = DRUPAL_ROOT . '/includes/bootstrap.inc'; + $key = sha1(serialize($databases) . filectime($filepath) . fileinode($filepath), TRUE); + $time_diff = REQUEST_TIME - $time; + // Since we are making a local request, a 2 second time window is allowed, + // and the HMAC must match. + return (($time_diff >= 0) && ($time_diff < 3) && ($hmac == base64_encode(hash_hmac('sha1', $check_string, $key, TRUE)))); +} + +/** + * Generate a user agent string with a HMAC and timestamp for simpletest. + */ +function drupal_generate_test_ua($prefix) { + global $databases; + static $key; + + if (!isset($key)) { + // We use the database credentials to make the HMAC key, since we + // check the HMAC before the database is initialized. filectime() + // and fileinode() are not easily determined from remote. + $filepath = DRUPAL_ROOT . '/includes/bootstrap.inc'; + $key = sha1(serialize($databases) . filectime($filepath) . fileinode($filepath), TRUE); + } + // Generate a moderately secure HMAC based on the database credentials. + $salt = uniqid('', TRUE); + $check_string = $prefix . ';' . time() . ';' . $salt; + return $check_string . ';' . base64_encode(hash_hmac('sha1', $check_string, $key, TRUE)); +} + +/** * Enables use of the theme system without requiring database access. * * Loads and initializes the theme system for site installs, updates and when diff --git a/includes/common.inc b/includes/common.inc index 4f36a7d8d..0906f8714 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -552,8 +552,8 @@ function drupal_http_request($url, array $options = array()) { // user-agent is used to ensure that multiple testing sessions running at the // same time won't interfere with each other as they would if the database // prefix were stored statically in a file or database variable. - if (is_string($db_prefix) && preg_match("/^simpletest\d+/", $db_prefix, $matches)) { - $options['headers']['User-Agent'] = $matches[0]; + if (is_string($db_prefix) && preg_match("/simpletest\d+/", $db_prefix, $matches)) { + $options['headers']['User-Agent'] = drupal_generate_test_ua($matches[0]); } $request = $options['method'] . ' ' . $path . " HTTP/1.0\r\n"; @@ -809,7 +809,7 @@ function _drupal_log_error($error, $fatal = FALSE) { // When running inside the testing framework, we relay the errors // to the tested site by the way of HTTP headers. - if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^simpletest\d+$/", $_SERVER['HTTP_USER_AGENT']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { + if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^simpletest\d+;/", $_SERVER['HTTP_USER_AGENT']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { // $number does not use drupal_static as it should not be reset // as it uniquely identifies each PHP error. static $number = 0; diff --git a/includes/database/database.inc b/includes/database/database.inc index 69f84a446..c18aec2f5 100644 --- a/includes/database/database.inc +++ b/includes/database/database.inc @@ -1347,9 +1347,10 @@ abstract class Database { } // We need to pass around the simpletest database prefix in the request - // and we put that in the user_agent header. - if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^simpletest\d+$/", $_SERVER['HTTP_USER_AGENT'])) { - $db_prefix .= $_SERVER['HTTP_USER_AGENT']; + // and we put that in the user_agent header. The header HMAC was already + // validated in bootstrap.inc. + if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\d+);/", $_SERVER['HTTP_USER_AGENT'], $matches)) { + $db_prefix .= $matches[1]; } return $new_connection; } diff --git a/install.php b/install.php index 30c2de029..ce0b3c6dc 100644 --- a/install.php +++ b/install.php @@ -28,7 +28,7 @@ function install_main() { // The user agent header is used to pass a database prefix in the request when // running tests. However, for security reasons, it is imperative that no // installation be permitted using such a prefix. - if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^simpletest\d+$/", $_SERVER['HTTP_USER_AGENT'])) { + if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], "simpletest") !== FALSE) { header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); exit; } diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index 8b5655d51..f04c99ec0 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -1194,6 +1194,7 @@ class DrupalWebTestCase extends DrupalTestCase { */ protected function curlInitialize() { global $base_url, $db_prefix; + if (!isset($this->curlHandle)) { $this->curlHandle = curl_init(); $curl_options = $this->additionalCurlOptions + array( @@ -1206,9 +1207,6 @@ class DrupalWebTestCase extends DrupalTestCase { CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on https. CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'), ); - if (preg_match('/simpletest\d+/', $db_prefix, $matches)) { - $curl_options[CURLOPT_USERAGENT] = $matches[0]; - } if (isset($this->httpauth_credentials)) { $curl_options[CURLOPT_USERPWD] = $this->httpauth_credentials; } @@ -1217,6 +1215,11 @@ class DrupalWebTestCase extends DrupalTestCase { // By default, the child session name should be the same as the parent. $this->session_name = session_name(); } + // We set the user agent header on each request so as to use the current + // time and a new uniqid. + if (preg_match('/simpletest\d+/', $db_prefix, $matches)) { + curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($matches[0])); + } } /** diff --git a/modules/simpletest/simpletest.install b/modules/simpletest/simpletest.install index 6d77ff14e..42d625c93 100644 --- a/modules/simpletest/simpletest.install +++ b/modules/simpletest/simpletest.install @@ -130,6 +130,7 @@ function simpletest_requirements($phase) { $t = get_t(); $has_curl = function_exists('curl_init'); + $has_hash = function_exists('hash_hmac'); $has_domdocument = class_exists('DOMDocument'); $requirements['curl'] = array( @@ -140,6 +141,14 @@ function simpletest_requirements($phase) { $requirements['curl']['severity'] = REQUIREMENT_ERROR; $requirements['curl']['description'] = $t('Simpletest could not be installed because the PHP <a href="@curl_url">cURL</a> library is not available.', array('@curl_url' => 'http://php.net/manual/en/curl.setup.php')); } + $requirements['hash'] = array( + 'title' => $t('hash'), + 'value' => $has_hash ? $t('Enabled') : $t('Not found'), + ); + if (!$has_hash) { + $requirements['hash']['severity'] = REQUIREMENT_ERROR; + $requirements['hash']['description'] = $t('Simpletest could not be installed because the PHP <a href="@hash_url">hash</a> extension is disabled.', array('@hash_url' => 'http://php.net/manual/en/book.hash.php')); + } $requirements['php_domdocument'] = array( 'title' => $t('PHP DOMDocument class'), |