diff --git a/includes/session.inc b/includes/session.inc
index 1f3c1773ba..a174290409 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('hashed_session_ids_supported', 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..7b0a50280c 100644
--- a/modules/simpletest/tests/session.test
+++ b/modules/simpletest/tests/session.test
@@ -246,6 +246,68 @@ class SessionTestCase extends DrupalWebTestCase {
     $this->assertResponse(403, 'An empty session ID is not allowed.');
   }
 
+  /**
+   * Test hashing of session ids in the database.
+   */
+  function testHashedSessionIds() {
+    $user = $this->drupalCreateUser(array('access content'));
+    $this->drupalLogin($user);
+    $this->drupalGet('session-test/is-logged-in');
+    $this->assertResponse(200, 'User is logged in.');
+
+    $this->drupalGet('session-test/id');
+    $matches = array();
+    preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches);
+    $this->assertTrue(!empty($matches[1]) , 'Found session ID after logging in.');
+    $session_id = $matches[1];
+
+    $this->drupalGet('session-test/id-from-cookie');
+    $matches = array();
+    preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches);
+    $this->assertTrue(!empty($matches[1]) , 'Found session ID from cookie.');
+    $cookie_session_id = $matches[1];
+
+    $this->assertEqual($session_id, $cookie_session_id, 'Session id and cookie session id are the same.');
+
+    $sql = 'SELECT s.sid FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE u.uid = :uid';
+    $db_session = db_query($sql, array(':uid' => $user->uid))->fetchObject();
+
+    $this->assertNotEqual($db_session->sid, $cookie_session_id, 'Session id in the database is not the same as in the session cookie.');
+    $this->assertEqual($db_session->sid, drupal_hash_base64($cookie_session_id), 'Session id in the database is the cookie session id hashed.');
+  }
+
+  /**
+   * Test opt-out of hashing of session ids in the database.
+   */
+  function testHashedSessionIdsOptOut() {
+    variable_set('do_not_hash_session_ids', TRUE);
+
+    $user = $this->drupalCreateUser(array('access content'));
+    $this->drupalLogin($user);
+    $this->drupalGet('session-test/is-logged-in');
+    $this->assertResponse(200, 'User is logged in.');
+
+    $this->drupalGet('session-test/id');
+    $matches = array();
+    preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches);
+    $this->assertTrue(!empty($matches[1]) , 'Found session ID after logging in.');
+    $session_id = $matches[1];
+
+    $this->drupalGet('session-test/id-from-cookie');
+    $matches = array();
+    preg_match('/\s*session_id:(.*)\n/', $this->drupalGetContent(), $matches);
+    $this->assertTrue(!empty($matches[1]) , 'Found session ID from cookie.');
+    $cookie_session_id = $matches[1];
+
+    $this->assertEqual($session_id, $cookie_session_id, 'Session id and cookie session id are the same.');
+
+    $sql = 'SELECT s.sid FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE u.uid = :uid';
+    $db_session = db_query($sql, array(':uid' => $user->uid))->fetchObject();
+
+    $this->assertEqual($db_session->sid, $cookie_session_id, 'Session id in the database is the same as in the session cookie.');
+    $this->assertNotEqual($db_session->sid, drupal_hash_base64($cookie_session_id), 'Session id in the database is not the cookie session id hashed.');
+  }
+
   /**
    * Test absence of SameSite attribute on session cookies by default.
    */
@@ -740,8 +802,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..0f55c501c7 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('hashed_session_ids_supported', 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');
 }
 
+/**
+ * Prepare the schema and data of the sessions table for hashed session ids.
+ */
+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('hashed_session_ids_supported', 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..3a6576dc51 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;
