diff --git a/includes/session.inc b/includes/session.inc index 1f3c1773ba..8e03f71855 100644 --- a/includes/session.inc +++ b/includes/session.inc @@ -87,19 +87,21 @@ function _drupal_session_read($sid) { // Otherwise, if the session is still active, we have a record of the // client's session in the database. If it's HTTPS then we are either have // a HTTPS session or we are about to log in so we check the sessions table - // for an anonymous session with the non-HTTPS-only cookie. + // for an anonymous session with the non-HTTPS-only cookie. The session ID + // that is in the user's cookie is hashed before being stored in the database + // as a security measure. Thus, we have to hash it to match the database. if ($is_https) { - $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid", array(':ssid' => $sid))->fetchObject(); + $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid", array(':ssid' => drupal_session_id($sid)))->fetchObject(); if (!$user) { if (isset($_COOKIE[$insecure_session_name])) { $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid AND s.uid = 0", array( - ':sid' => $_COOKIE[$insecure_session_name])) + ':sid' => drupal_session_id($_COOKIE[$insecure_session_name]))) ->fetchObject(); } } } else { - $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => $sid))->fetchObject(); + $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => drupal_session_id($sid)))->fetchObject(); } // We found the client's session record and they are an authenticated, @@ -185,17 +187,18 @@ function _drupal_session_write($sid, $value) { // Use the session ID as 'sid' and an empty string as 'ssid' by default. // _drupal_session_read() does not allow empty strings so that's a safe // default. - $key = array('sid' => $sid, 'ssid' => ''); + $key = array('sid' => drupal_session_id($sid), 'ssid' => ''); // On HTTPS connections, use the session ID as both 'sid' and 'ssid'. if ($is_https) { - $key['ssid'] = $sid; + $key['ssid'] = drupal_session_id($sid); // The "secure pages" setting allows a site to simultaneously use both // secure and insecure session cookies. If enabled and both cookies are - // presented then use both keys. + // presented then use both keys. The session ID from the cookie is + // hashed before being stored in the database as a security measure. if (variable_get('https', FALSE)) { $insecure_session_name = substr(session_name(), 1); if (isset($_COOKIE[$insecure_session_name])) { - $key['sid'] = $_COOKIE[$insecure_session_name]; + $key['sid'] = drupal_session_id($_COOKIE[$insecure_session_name]); } } } @@ -416,18 +419,18 @@ function drupal_session_regenerate() { 'httponly' => $params['httponly'], ); drupal_setcookie(session_name(), session_id(), $options); - $fields = array('sid' => session_id()); + $fields = array('sid' => drupal_session_id(session_id())); if ($is_https) { - $fields['ssid'] = session_id(); + $fields['ssid'] = drupal_session_id(session_id()); // If the "secure pages" setting is enabled, use the newly-created // insecure session identifier as the regenerated sid. if (variable_get('https', FALSE)) { - $fields['sid'] = $session_id; + $fields['sid'] = drupal_session_id($session_id); } } db_update('sessions') ->fields($fields) - ->condition($is_https ? 'ssid' : 'sid', $old_session_id) + ->condition($is_https ? 'ssid' : 'sid', drupal_session_id($old_session_id)) ->execute(); } elseif (isset($old_insecure_session_id)) { @@ -435,8 +438,8 @@ function drupal_session_regenerate() { // secure site but a session was active on the insecure site, update the // insecure session with the new session identifiers. db_update('sessions') - ->fields(array('sid' => $session_id, 'ssid' => session_id())) - ->condition('sid', $old_insecure_session_id) + ->fields(array('sid' => drupal_session_id($session_id), 'ssid' => drupal_session_id(session_id()))) + ->condition('sid', drupal_session_id($old_insecure_session_id)) ->execute(); } else { @@ -488,7 +491,7 @@ function _drupal_session_destroy($sid) { // Delete session data. db_delete('sessions') - ->condition($is_https ? 'ssid' : 'sid', $sid) + ->condition($is_https ? 'ssid' : 'sid', drupal_session_id($sid)) ->execute(); // Reset $_SESSION and $user to prevent a new session from being started @@ -598,3 +601,22 @@ function drupal_save_session($status = NULL) { } return $save_session; } + +/** + * Session ids are hashed by default before being stored in the database. + * + * This should only be done if any existing sessions have been updated, as + * reflected by the hash_session_ids variable. + * + * @param $id + * A session id. + * + * @return + * The session id which may have been hashed. + */ +function drupal_session_id($id) { + if (variable_get('hash_session_ids', FALSE) && !variable_get('do_not_hash_session_ids', FALSE)) { + $id = drupal_hash_base64($id); + } + return $id; +} diff --git a/modules/simpletest/tests/session.test b/modules/simpletest/tests/session.test index 05c98c1182..4ac14b09ff 100644 --- a/modules/simpletest/tests/session.test +++ b/modules/simpletest/tests/session.test @@ -740,8 +740,8 @@ class SessionHttpsTestCase extends DrupalWebTestCase { */ protected function assertSessionIds($sid, $ssid, $assertion_text) { $args = array( - ':sid' => $sid, - ':ssid' => $ssid, + ':sid' => drupal_session_id($sid), + ':ssid' => !empty($ssid) ? drupal_session_id($ssid) : '', ); return $this->assertTrue(db_query('SELECT timestamp FROM {sessions} WHERE sid = :sid AND ssid = :ssid', $args)->fetchField(), $assertion_text); } diff --git a/modules/system/system.install b/modules/system/system.install index 4c567a2764..209967fb16 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -671,6 +671,9 @@ function system_install() { // Populate the cron key variable. $cron_key = drupal_random_key(); variable_set('cron_key', $cron_key); + + // This variable indicates that the database is ready for hashed session ids. + variable_set('hash_session_ids', TRUE); } /** @@ -1611,13 +1614,13 @@ function system_schema() { 'not null' => TRUE, ), 'sid' => array( - 'description' => "A session ID. The value is generated by Drupal's session handlers.", + 'description' => "A session ID (hashed). The value is generated by Drupal's session handlers.", 'type' => 'varchar', 'length' => 128, 'not null' => TRUE, ), 'ssid' => array( - 'description' => "Secure session ID. The value is generated by Drupal's session handlers.", + 'description' => "Secure session ID (hashed). The value is generated by Drupal's session handlers.", 'type' => 'varchar', 'length' => 128, 'not null' => TRUE, @@ -3360,6 +3363,54 @@ function system_update_7085() { variable_del('block_interest_cohort'); } +/* + * Update the schema and data of the sessions table. + */ +function system_update_7086() { + // Update the session ID fields' description. + $spec = array( + 'description' => "A session ID (hashed). The value is generated by Drupal's session handlers.", + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + ); + db_drop_primary_key('sessions'); + db_change_field('sessions', 'sid', 'sid', $spec, array('primary key' => array('sid', 'ssid'))); + // Updates the secure session ID field's description. + $spec = array( + 'description' => "Secure session ID (hashed). The value is generated by Drupal's session handlers.", + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ); + db_drop_primary_key('sessions'); + db_change_field('sessions', 'ssid', 'ssid', $spec, array('primary key' => array('sid', 'ssid'))); + + // Update all existing sessions. + if (!variable_get('do_not_hash_session_ids', FALSE)) { + $sessions = db_query('SELECT sid, ssid FROM {sessions}'); + while ($session = $sessions->fetchAssoc()) { + $query = db_update('sessions'); + $fields = array(); + if (!empty($session['sid'])) { + $fields['sid'] = drupal_hash_base64($session['sid']); + $query->condition('sid', $session['sid']); + } + if (!empty($session['ssid'])) { + $fields['ssid'] = drupal_hash_base64($session['ssid']); + $query->condition('ssid', $session['ssid']); + } + $query + ->fields($fields) + ->execute(); + } + } + + // This variable indicates that the database is ready for hashed session ids. + variable_set('hash_session_ids', TRUE); +} + /** * @} End of "defgroup updates-7.x-extra". * The next series of updates should start at 8000. diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index a0ce6ddf6f..22447ef158 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -854,3 +854,11 @@ $conf['mail_display_name_site_name'] = TRUE; * @see https://www.php.net/manual/function.phpinfo.php */ # $conf['sa_core_2023_004_phpinfo_flags'] = ~(INFO_VARIABLES | INFO_ENVIRONMENT); + +/** + * Session ids are hashed by default before being stored in the database. This + * reduces the risk of sessions being hijacked if the database is compromised. + * + * This variable allows opting out of this security improvement. + */ +# $conf['do_not_hash_session_ids'] = TRUE;