Background

  1. The session id is a random value generated when a session is started. The session id is stored as a cookie in the browser such that on subsequent visits the data stored in the session can be loaded and reused. This issue is about the session id (cookie value) and not about the session name (cookie name).
  2. Drupal does not rely on built-in PHP methods for generating the session id currently, but uses a random 32 byte value.

  3. Before Drupal 7.24 different methods were used for anonymous vs. authenticated users, such that anonymous session ids were less expensive to generate.

    (see this commit; also note that the comment about "less random sessions" is wrong now).

  4. In contrast Symfony does not expect that the session-id is changed manually after the session has been started (see [...]\Session\Storage\Proxy\AbstractProxy::setId() and completely relies on the PHP built-in function session_regenerate_id() (see [...]\Session\Storage\NativeSessionStorage::regenerate().

Problem

  1. Our approach does not allow us to use session_regenerate_id().
  2. Instead, it is necessary to override NativeSessionStorage::regenerate completely and re-implement the mechanism in custom code (see #801278: Authenticated users getting "less random" session IDs).

Proposed solution

  1. PHP 5.4 and subsequently PHP 7.1 ship with improvements regarding session ID generation:

    1. The INI setting session.entropy_file defaults to /dev/urandom or /dev/arandom if it is available (session.c (5.4) vs session.c (5.3)). On Windows, the Random API is used.
    2. On machines with /dev/urandom or /dev/arandom present on compile time session.entropy_length defaults to 32. On Windows, this seemingly has to be configured manually.
    3. Using session.sid_length, the length of the generated session id can be specified
    4. With session.sid_bits_per_character it is possible to choose the alphabet for the generated session id
  2. According to the OWASP PHP Security Cheat Sheet:

    PHP's default session facilities are considered safe, the generated PHPSessionID is random enough, but the storage is not necessarily safe.

  3. When using the Drupal Crypt::randomBytes() function, the best (most secure and most performing) random source is selected automatically. The performance of both approaches was analyzed in #9. According to the results, no performance regression is to be expected when switching to PHP built-in session id generation.
  4. → Remove our custom code for generating session IDs and rely on the native PHP functionality instead.

User interface changes

None

API changes

  • The protected method \Drupal\Core\Session\SessionManager::migrateStoredSession() is removed.
  • Getting a session ID via session_id()/\Drupal\Core\Session\SessionManager::getId() before the session is properly started is deprecated.
  • Drupal now supports defining the container parameters sid_length<code> and <code>sid_bits_per_character in a site's services.yml. This default to 48 and 6 respectively.
  • \Drupal\Core\TempStore\SharedTempStore and \Drupal\Core\TempStore\SharedTempStoreFactory need the current user injected into the constructor.

Data model changes

None.

Release notes snippet

Drupal now uses PHPs built session generation. The change record details how custom and contributed code that relies on the session ID should be updated.

CommentFileSizeAuthor
#164 2238561-4-164.patch32.68 KBalexpott
#164 2238561-4-user-edit-test-only.patch947 bytesalexpott
#164 160-164-interdiff.txt5.15 KBalexpott
#160 2238561-4-160.patch32.28 KBalexpott
#160 155-160-interdiff.txt2.62 KBalexpott
#155 2238561-4-155.patch32.4 KBalexpott
#155 153-155-interdiff.txt789 bytesalexpott
#153 2238561-4-153.patch33.17 KBalexpott
#153 150-153-interdiff.txt1.62 KBalexpott
#150 2238561-4-150.patch33.14 KBalexpott
#150 2238561-4-150-test-only.patch32.46 KBalexpott
#150 148-150-interdiff.txt3.03 KBalexpott
#148 2238561-4-148.patch30.57 KBalexpott
#148 144-148-interdiff.txt4.53 KBalexpott
#144 2238561-4-144.patch26.19 KBalexpott
#144 141-144-interdiff.txt1.08 KBalexpott
#141 interdiff-2238561-141.txt936 bytesdawehner
#141 2238561-141.patch26.2 KBdawehner
#139 2238561-4-139.patch26.19 KBalexpott
#139 136-139-interdiff.txt5.65 KBalexpott
#136 Screen Shot 2020-12-11 at 16.21.53.jpg219.17 KBdawehner
#136 interdiff-2238561-133-136.txt4.59 KBdawehner
#136 2238561-136.patch25.14 KBdawehner
#133 interdiff-2238561.txt4.03 KBdawehner
#133 2238561-133.patch21.23 KBdawehner
#132 interdiff-2238561.txt4.73 KBdawehner
#132 2238561-132.patch21.68 KBdawehner
#130 2238561-130.patch21.21 KBdawehner
#119 2238561-9.1.x-119.patch21.28 KBalexpott
#119 117-119-interdiff.txt4.97 KBalexpott
#117 2238561-9.1.x-117.patch19.87 KBalexpott
#117 115-117-interdiff.txt802 bytesalexpott
#115 2238561-9.1.x-115.patch19.72 KBalexpott
#115 113-115-interdiff.txt596 bytesalexpott
#113 2238561-9.1.x-113.patch19.73 KBalexpott
#113 111-113-interdiff.txt4.14 KBalexpott
#111 interdiff-106-111.txt869 byteseiriksm
#111 2238561-111.patch20.36 KBeiriksm
#106 interdiff_103-106.txt788 bytesridhimaabrol24
#106 2238561-106.patch20.62 KBridhimaabrol24
#105 2238561-105.patch0 bytesjofitz
#105 interdiff-2238561-103-105.txt13.22 KBjofitz
#103 2238561-103.patch20.62 KBridhimaabrol24
#94 2238561-3-94.patch27.76 KBalexpott
#94 92-94-interdiff.txt1.33 KBalexpott
#93 2238561-3-93.patch28.47 KBalexpott
#93 92-93-interdiff.txt1.97 KBalexpott
#92 interdiff-2.txt1.81 KBbradjones1
#92 interdiff.txt1.62 KBbradjones1
#92 php-session-id-2238561-92.patch28.24 KBbradjones1
#90 php-session-id-2238561-90.patch26.98 KBbradjones1
#84 php-session-id-2238561-84.patch28.08 KBDarren Oh
#77 php-session-id-2238561-77.patch19.19 KBkalyansamanta
#73 2238561-73.patch26.43 KBalexpott
#73 70-73-interdiff.txt653 bytesalexpott
#70 2238561-70.patch25.97 KBalexpott
#70 69-70-interdiff.txt15.38 KBalexpott
#69 2238561-69.patch19.18 KBalexpott
#69 66-69-interdiff.txt474 bytesalexpott
#66 2238561-66.patch19.18 KBalexpott
#66 62-66-interdiff.txt738 bytesalexpott
#62 2238561-62.patch18.46 KBalexpott
#62 60-62-interdiff.txt9.99 KBalexpott
#60 2238561-60.patch9.93 KBalexpott
#60 57-60-interdiff.txt4.21 KBalexpott
#57 2238561-57.patch8.36 KBalexpott
#57 53-57-interdiff.txt4.38 KBalexpott
#53 2238561-53.patch5.77 KBalexpott
#53 51-53-interdiff.txt2 KBalexpott
#51 2238561-51.patch5.3 KBjofitz
#37 2238561-session_id-37.patch8.31 KBandypost
#37 interdiff.txt3.93 KBandypost
#35 2238561-session_id-35.patch5.14 KBandypost
#35 interdiff.txt5.65 KBandypost
#29 2238561-session_id-29.patch9.28 KBandypost
#29 interdiff.txt3.7 KBandypost
#25 2238561-session_id-25.patch7.77 KBandypost
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

sun’s picture

Title: Consider using the default PHP session id instead of generating our own » Use the default PHP session ID instead of generating a custom one
Issue summary: View changes
Issue tags: +PHP 5.4
Parent issue: » #1858196: [meta] Leverage Symfony Session components
Related issues: +#2238087: Rebase SessionManager onto Symfony NativeSessionStorage, +#801278: Authenticated users getting "less random" session IDs

Adding some structure to the issue summary.

sun’s picture

Issue summary: View changes
moshe weitzman’s picture

Makes sense to me.

sun’s picture

There are some user comments that outline a potential pitfall: Multiple concurrent sessions.

Do we have use-cases for multiple concurrent sessions in Drupal? E.g., Bakery?

pwolanin’s picture

Status: Active » Postponed

No, let's do this issue first instead: #2164025: Improve security of session ID against DB exposure or SQL injection. I'm not sure it's compatible, and it's certainly higher priority

We also currently use a faster random source from openssl if it's available, so I'm not sure this is a good idea from a performance perspective.

pwolanin’s picture

Status: Postponed » Active

sorry, I'm just confusing myself. The hardening one is already in 8.x. However, I'm not sure about the performance implications of this proposed change.

Also, it seems a host or user could make mistakes with their ini settings?

znerol’s picture

Issue summary: View changes
znerol’s picture

Issue summary: View changes
znerol’s picture

Okay, I propose the following scripts for performance analyzes:

  1. Test for drupal_random_key() (rkey.php):
    
    define('DRUPAL_ROOT', getcwd());
    include_once DRUPAL_ROOT . '/includes/bootstrap.inc';
    drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
    
    session_start();
    
    $start = microtime(TRUE);
    for ($i = 0; $i < 1000; $i++) {
      drupal_random_key();
    }
    print (microtime(TRUE) - $start);
    
  2. Test for session_regenerate_id() (regen.php):
    
    define('DRUPAL_ROOT', getcwd());
    include_once DRUPAL_ROOT . '/includes/bootstrap.inc';
    drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
    
    session_start();
    
    $start = microtime(TRUE);
    for ($i = 0; $i < 1000; $i++) {
      session_regenerate_id();
    }
    // Remove all set-cookie headers.
    header_remove();
    print (microtime(TRUE) - $start);
    

Then use ab -n500 http://your-server/rkey.php and ab -n500 http://your-server/regen.php

My results:

  1. For drupal_random_key(): Requests per second: 50.16 [#/sec] (mean)
  2. For session_regenerate_id(): Requests per second: 74.97 [#/sec] (mean)

Relevant PHP ini settings:

session.entropy_file => /dev/urandom
session.entropy_length => 32 => 32
session.hash_function => 1 => 1
OpenSSL support => enabled

I had to manually set the hash function, such that SHA-1 is used instead of MD5, the other settings are defaults on my system.

PHP version: PHP 5.4.27-1~dotdeb.1

znerol’s picture

Also I'd like to point out that when we get rid of the custom session-ids, we do not need to generate the id for lazy sessions anymore (See SessionManager::initialize()).

However we'd need to make sure that nothing tries to retrieve session_id() when no session is started. This is especially true for the CsrfTokenGenerator (however there is a solution to that problem: #961508-272: Dual http/https session cookies interfere with drupal_valid_token()).

drumm’s picture

Bakery adds cookies, like CHOCOLATECHIPSSL, which would be unaffected.

Bakery does hand off to Drupal core's sessions after checking its cookies. There is a separate session cookie for each site. As long as the cookie names are still unique per-site, this should be okay. I assume we won't be going with PHPSESSID, that would open Drupal up to all sorts of hard to debug situations, like if other PHP software is running on the same domain, or a subdomain.

pwolanin’s picture

Hmm, so lots of red flags in the settings above - such as having to set the hash function. I feel liek this is opening up a lot of potential variability as Drupal is deployed on different platforms with different ini settings.

Also, changing the cookie name seems like a very bad idea. If that's required for this change, I'd be opposed. Also, what about the mixed mode and SSL-only session handling?

Damien Tournoud’s picture

Status: Active » Postponed

As already noted by @znerol, we generate the session ID ourselves because of the lazy session implementation.

So this cannot change until/if something like #961508-272: Dual http/https session cookies interfere with drupal_valid_token() lands. Marking as postponed.

Note that the benchmark in #9 shows a mere 10us difference between PHP implementation and ours, not even close to a real performance improvement.

znerol’s picture

Title: Use the default PHP session ID instead of generating a custom one » Consider using the default PHP session ID instead of generating a custom one

Re #11/#12: This is not about session_name() (= cookie name) but about session_id() (the cookie value).

Re #12: We always used to hard-code some session-related ini-settings in settings.php. Mixed mode SSL will not be affected by this change, I think.

Re #13: We do not need to give up lazy sessions at all with this change, we only need to fix code that assumes a session id will always be set. The performance test was done in response to #5. It shows that we do not have to fear a performance regression when switching from our session-ids to the PHP built-in ones.

Note that keeping our session-id implementation does not really block #2238087: Rebase SessionManager onto Symfony NativeSessionStorage, but just makes it harder to implement it such that it complies completely with SessionStorageInterface::regenerate().

znerol’s picture

Issue summary: View changes
znerol’s picture

Issue summary: View changes
znerol’s picture

Issue summary: View changes
jibran’s picture

Status: Postponed » Active

Back to active again.

znerol’s picture

There is still one instance in core where the session_id() is used in a similar way like it was before in the CSRF. This is in Drupal\user\TempStoreFactory. In order to eliminate the session_id() call there it is either necessary to make the $owner parameter of the get() method required and delegate the responsibility of determining the owner to the caller, or to implement a similar method as #2245003: Use a random seed instead of the session_id for CSRF token generation.

znerol’s picture

Issue summary: View changes
znerol’s picture

Issue summary: View changes
jibran’s picture

All the related issues are fixed.

andypost’s picture

Also after #2421263: Potential data loss: concurrent (i.e. by different users) node edits leak through preview there's only \Drupal\user\SharedTempStoreFactory::get() uses session_id() and \Drupal\user\PrivateTempStore::getOwner() uses $this->requestStack->getCurrentRequest()->getSession()->getId() so it makes sense to replace.

andypost’s picture

Status: Active » Needs review
FileSize
7.77 KB

Initial code

PS: once Core require 5.5 we can implement own http://php.net/manual/en/sessionhandler.create-sid.php

Status: Needs review » Needs work

The last submitted patch, 25: 2238561-session_id-25.patch, failed testing.

xjm’s picture

Priority: Normal » Major
znerol’s picture

andypost’s picture

Status: Needs work » Needs review
FileSize
3.7 KB
9.28 KB

WIP - clean-up a bit, and add @todo to fix properly

PS: Drupal\Tests\user\Unit\PrivateTempStoreTest still broken

Status: Needs review » Needs work

The last submitted patch, 29: 2238561-session_id-29.patch, failed testing.

znerol’s picture

+++ b/core/lib/Drupal/Core/Session/SessionManager.php
@@ -223,7 +213,7 @@ public function regenerate($destroy = FALSE, $lifetime = NULL) {
-    session_id(Crypt::randomBytesBase64());
+    session_regenerate_id();

I think we should delegate that to the parent class. Also note that the switch from session_id() to regenerate_session_id() will also allow us to remove almost all the other cruft from this method. For example startNow is not necessary anymore because regenerate does not touch the content of $_SESSION. It also frees us from explicitly sending the cookie. Migration of the session data is also not necessary if we simply pass in the $destroy = TRUE upon login (this will remove the old entry from storage, but will not touch existing data in $_SESSION). As a result we should be able to reduce the method to something like this:

  public function regenerate($destroy = FALSE, $lifetime = NULL) {
    // Nothing to do if we are not allowed to change the session.
    if ($this->isCli()) {
      return;
    }

    $result = parent::regenerate($destroy, $lifetime);
    $this->getMetadataBag()->clearCsrfTokenSeed();

    return $result;
  }

It might even be possible to eliminate the clearCsrfTokenSeed() by simply overriding stampNew() in the drupal Session\MetadataBag.

znerol’s picture

Oh, and together with #2472535: Remove SessionManager::delete in favor of a portable mechanism to invalid sessions of authenticated users this will remove all database queries from SessionManager and as a result contrib session storage modules will only need to override SessionHandler.

andypost’s picture

Status: Needs work » Needs review

It looks we need to start session if temp store accessed by anonymous user without session, then set some cookie on response to give ability to get data.
tl'dr maybe separate issue?

znerol’s picture

Test failures in #29 is due to the removal of requestStack from tempstore, but meanwhile a patch went in which requires it to retrieve REQUEST_TIME.

If you use $request->getSession()->get('bla') and ->set('foo', 'bar') a session is automatically started. It is automatically removed as soon as the session becomes empty again.

andypost’s picture

Assigned: Unassigned » andypost
FileSize
5.65 KB
5.14 KB

revert removal of request stack, should fix some tests
working on #31

Status: Needs review » Needs work

The last submitted patch, 35: 2238561-session_id-35.patch, failed testing.

andypost’s picture

Assigned: andypost » Unassigned
Status: Needs work » Needs review
FileSize
3.93 KB
8.31 KB

Addressed #31, let's see...
PS: somehow installer tests are failed

Status: Needs review » Needs work

The last submitted patch, 37: 2238561-session_id-37.patch, failed testing.

cilefen’s picture

Issue tags: +Needs beta evaluation

This issue needs a beta evaluation if we want to have this in when Drupal 8 launches. I am concerned that this may be too disruptive based on its nature.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

lainosantos’s picture

Hello everyone. Any news here?

andypost’s picture

Version: 8.2.x-dev » 8.3.x-dev
Issue tags: -PHP 5.4, -Needs beta evaluation

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

skyredwang’s picture

The current session id generation allows the character "_" (underscore) in this function:

public static function randomBytesBase64($count = 32) {
    return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode(static::randomBytes($count)));
  }

But, PHP session id doesn't allow this "_" character, otherwise, it would throw error:

Warning: SessionHandler::write(): The session id is too long or contains illegal characters, valid characters are a-z, A-Z, 0-9 and '-,' in Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy->write() 

I am trying to understand how the session ids generated by the current system (up to Core 8.3.2) is still working.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

zessx’s picture

The offending class is \Drupal\Core\Session\SessionManager:

public function start() {
  ...
  $this->setId(Crypt::randomBytesBase64());
  ...
}
public function regenerate($destroy = FALSE, $lifetime = NULL) {
  ...
  session_id(Crypt::randomBytesBase64());
  ...
}

As this class is loaded via a service, you can (waiting for the fix) override it in a custom module, and define your own session_manager service:

# web/modules/custom/session_manager/session_manager.services.yml
services:
  session_handler.write_safe:
    class: Drupal\Core\Session\WriteSafeSessionHandler
    tags:
      - { name: session_handler_proxy, priority: 150 }
  session_manager:
    class: Drupal\session_manager\Session\SessionManager
    arguments: ['@request_stack', '@database', '@session_manager.metadata_bag', '@session_configuration', '@session_handler']
    tags:
      - { name: backend_overridable }
    calls:
      - [setWriteSafeHandler, ['@session_handler.write_safe']]

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

voleger’s picture

Issue tags: +Needs reroll
jofitz’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
5.3 KB

Re-rolled.

Status: Needs review » Needs work

The last submitted patch, 51: 2238561-51.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

alexpott’s picture

Status: Needs work » Needs review
FileSize
2 KB
5.77 KB

Trying to fix the tests.

I'm not entire sure we can make this change in 8.x since we'll break any code in contrib that assumes session_id() will return a valid value after calling SessionManager::start().

Status: Needs review » Needs work

The last submitted patch, 53: 2238561-53.patch, failed testing. View results

cosmicdreams’s picture

  1. +++ b/core/lib/Drupal/Core/TempStore/SharedTempStoreFactory.php
    @@ -76,7 +76,20 @@ public function get($collection, $owner = NULL) {
    +      $account = \Drupal::currentUser();
    

    Technically, this can be injected as the 'current_user' service.

  2. +++ b/core/lib/Drupal/Core/TempStore/SharedTempStoreFactory.php
    @@ -76,7 +76,20 @@ public function get($collection, $owner = NULL) {
    +          $session = \Drupal::service('session');
    

    We're injecting the Request but we aren't injecting the session service? Perhaps we should.

  3. +++ b/core/modules/user/user.module
    @@ -561,7 +561,7 @@ function user_login_finalize(UserInterface $account) {
    +  \Drupal::service('session')->migrate(TRUE);
    

    We use the session service again here so if we had injected it we would be able to reuse it here.

neclimdul’s picture

+++ b/core/includes/install.core.inc
@@ -1557,6 +1557,8 @@ function install_bootstrap_full() {
+  $_SESSION['_drupap_installing'] = TRUE;

s/drupap/drupal/?

alexpott’s picture

Status: Needs work » Needs review
FileSize
4.38 KB
8.36 KB

1. Sure but let's do injection once we've agreed this is the right approach.
2. I think the case where the request doesn't already have the session is probably something we shouldn't really support. So let's take the \Drupal\Core\TempStore\PrivateTempStore::startSession() approach and not inject.
3. This is procedural code so no inject but we don't need to do \Drupal twice.

I think we might need to do #2865991: provide an API to forcibly start a session.

+++ b/core/lib/Drupal/Core/TempStore/SharedTempStoreFactory.php
@@ -76,7 +76,20 @@ public function get($collection, $owner = NULL) {
+        if (!$has_session) {
+          /** @var \Symfony\Component\HttpFoundation\Session\SessionInterface $session */
+          $session = \Drupal::service('session');
+          $this->requestStack->getCurrentRequest()->setSession($session);
+          $session->start();
+        }
+        $owner = $this->requestStack->getCurrentRequest()->getSession()->getId();

Because of lazy sessions this now might not have an ID.

So in the patch I've converted this to setting a random string in session to use. Which will cause the session to be created - what we need to happen.

Still not convinced we can do this in Drupal 8 without something to make code like:

$session = \Drupal::service('session');
$this->requestStack->getCurrentRequest()->setSession($session);
$session->start();
$this->requestStack->getCurrentRequest()->getSession()->getId();

work for anonymous users. With this patch $this->requestStack->getCurrentRequest()->getSession()->getId(); will return an empty string because $session->start(); no longer calls session_id(some random value) and we do our lazy session thing.

alexpott’s picture

+++ b/core/includes/install.core.inc
@@ -1557,6 +1557,8 @@ function install_bootstrap_full() {
+  // Ensure the session is maintained throughout the install.
+  $_SESSION['_drupal_installing'] = TRUE;

+++ b/core/lib/Drupal/Core/Session/SessionManager.php
@@ -119,17 +118,6 @@ public function start() {
-      // Randomly generate a session identifier for this request. This is
-      // necessary because \Drupal\Core\TempStore\SharedTempStoreFactory::get()
-      // wants to know the future session ID of a lazily started session in
-      // advance.
-      //
-      // @todo: With current versions of PHP there is little reason to generate
-      //   the session id from within application code. Consider using the
-      //   default php session id instead of generating a custom one:
-      //   https://www.drupal.org/node/2238561
-      $this->setId(Crypt::randomBytesBase64());

If I revert the $this->setId(Crypt::randomBytesBase64()) then the installer change is not necessary. The really odd thing is that there doesn't appear to be any call to getId() or session_id() so I've got no idea why this matters.

Status: Needs review » Needs work

The last submitted patch, 57: 2238561-57.patch, failed testing. View results

alexpott’s picture

Status: Needs work » Needs review
FileSize
4.21 KB
9.93 KB

Lazy sessions and temp stores are such fun!

Status: Needs review » Needs work

The last submitted patch, 60: 2238561-60.patch, failed testing. View results

alexpott’s picture

Status: Needs work » Needs review
FileSize
9.99 KB
18.46 KB

Temp stores are still fun :)

catch’s picture

+++ b/core/includes/install.core.inc
@@ -1557,6 +1557,8 @@ function install_bootstrap_full() {
+  $_SESSION['_drupap_installing'] = TRUE;

drupap?

I'm not entire sure we can make this change in 8.x since we'll break any code in contrib that assumes session_id() will return a valid value after calling SessionManager::start().

This might be a case for contrib grepping, it feels like a behaviour rather than API change but it's a case of will it really break anything in practice or not.

alexpott’s picture

The drupap thing is fixed in the latest patch.

Of the contrib modules I have hanging around. I suspect search_api, ctools, devel, flag, cas will have something broken because of this change. Basically anything that uses the session ID as an identifier for anonymous users - since with this change it'll not be available unless you force a session save and re-start the session.

alexpott’s picture

I'm not convinced that \Drupal\Core\Cache\Context\SessionCacheContext will work the way we expect it to :(

alexpott’s picture

Let's see.

Status: Needs review » Needs work

The last submitted patch, 66: 2238561-66.patch, failed testing. View results

andypost’s picture

+++ b/core/includes/install.core.inc
@@ -1557,6 +1557,8 @@ function install_bootstrap_full() {
+  // Ensure the session is maintained throughout the install.
+  $_SESSION['_drupal_installing'] = TRUE;

Why this using global? maybe better to replace it with $session->set() to do less in #2473875: Convert uses of $_SESSION to symfony session retrieved from the request

alexpott’s picture

Status: Needs work » Needs review
FileSize
474 bytes
19.18 KB

@andypost - no reason - I threw it in there as a hack to see if it would fix it. Changes to use $session.

alexpott’s picture

Finished all the deprecation work and tested it all and started on the change records.

I'm not convinced this 8.6.x material which is problematic for PHP 7.3 compatibility in the 8.6.x cycle :(

I thought we might need to increase the storage for session IDs in the session table - see http://php.net/manual/en/session.configuration.php#ini.session.sid-length but since we do Crypt::hashBase64($sid) on the session ID before inserting it all increasing the length with do is increase $data that is hashed. I've tested with

session.sid_bits_per_character => 6 => 6
session.sid_length => 256 => 256

And I have wildly long session IDs in my browser and Drupal's session system is still working.

alexpott’s picture

catch’s picture

+++ b/core/includes/install.core.inc
@@ -1566,6 +1566,8 @@ function install_bootstrap_full() {
   $session->start();
+  // Ensure the session is maintained throughout the install.
+  $session->set('_drupal_installing', TRUE);
 }

Should this be removed at the end of installation?

Otherwise the patch looks OK but definitely too risky for 8.6.x - would be tempted to get it in earlier than later for 8.7.x though.

alexpott’s picture

@catch yep we can do that in install_finished()

larowlan’s picture

Of the contrib modules I have hanging around. I suspect search_api, ctools, devel, flag, cas will have something broken because of this change. Basically anything that uses the session ID as an identifier for anonymous users - since with this change it'll not be available unless you force a session save and re-start the session.

Is this still the case?

alexpott’s picture

@larowlan yep this code for anonymous flags...


      // If the user is anonymous, get the session ID.
      if ($account->isAnonymous()) {
        // Ensure something is in $_SESSION, otherwise the session ID will
        // not persist.
        // TODO: Replace this with something cleaner once core provides it.
        // See https://www.drupal.org/node/2865991.
        $_SESSION['flag'] = TRUE;

        $this->sessionManager->start();

        // Intentionally clobber $session_id; it makes no sense to specify that
        // but not $account.
        $session_id = $this->sessionManager->getId();
      }

in src/FlagService.php is not really correct. Flag passes the session ID all over the place.

andypost’s picture

kalyansamanta’s picture

FileSize
19.19 KB
kalyansamanta’s picture

Assigned: Unassigned » kalyansamanta
Status: Needs review » Fixed
tim.plunkett’s picture

Status: Fixed » Needs review
lokapujya’s picture

Can remove the word "Consider" from the issue title.

andypost’s picture

Title: Consider using the default PHP session ID instead of generating a custom one » Use the default PHP session ID instead of generating a custom one

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Darren Oh’s picture

Assigned: kalyansamanta » Darren Oh
Issue tags: -session +Seattle2019
Darren Oh’s picture

Assigned: Darren Oh » Unassigned
FileSize
28.08 KB

As far I as can tell, the patch in #77 did not consider any of the prior discussion in this issue, and it did not make any improvements. I am re-rolling the patch from #73 for review.

Darren Oh’s picture

I haven’t had time to test yet, but it looks like PHP 7.1 introduced a way to get the session ID before the session is active. We may be able to allow contributed modules to continue using the session ID to identify anonymous users.

alexpott’s picture

@Darren Oh unfortunately the example code hints that it might not be a perfect fit for us...

    // Call session_create_id() while session is active to 
    // make sure collision free.
    if (session_status() != PHP_SESSION_ACTIVE) {
        session_start();
    }
    // WARNING: Never use confidential strings for prefix!
    $newid = session_create_id('myprefix-');

That won't work with our lazy sessions where we don;t want to start a session unless 100% necessary.

Darren Oh’s picture

We don’t currently check for collisions when creating the session ID, so it would not be necessary to create an active session before generating the ID with session_create_id().

alexpott’s picture

@Darren Oh good point!

Darren Oh’s picture

Did a little testing for #1561866-49: Add support for built-in PHP session upload progress. Because PHP uses commas in base64 encoding for session identifiers, some of our tests will need to be updated to handle URL-encoded commas.

bradjones1’s picture

Re-rolled for 8.8.x. Let's see what testbot says.

Status: Needs review » Needs work

The last submitted patch, 90: php-session-id-2238561-90.patch, failed testing. View results

bradjones1’s picture

Status: Needs work » Needs review
FileSize
28.24 KB
1.62 KB
1.81 KB

The test failure above appears to be a deprecation issue in some unit tests; I've included an interdiff to address them (interdiff-2.txt)

The other change to this patch (interdiff.txt) is a contribution to clean up what I think is a confusing note in `user_logout()` regarding Session::invalidate(); I had opened an issue on cleaning this up at #3081643: Use Session::invalidate() in user_logout() and as I've looked at this more I think we can improve this comment here. In light of some of the cleanup in SessionManager we can explain why we are not using Session::invalidate(), even though the problems with it in 2015 have since been resolved. Basically, in light of the fact Drupal likes to clean up sessions, rather than regenerating them, we can't use the Symfony convenience method for ending a session as is otherwise suggested.

alexpott’s picture

The change to the installer was still bothering me. So I've finally tracked down why it's happening. It's because under certain conditions we start sending output early and that prevents the session being saved. Here's an alternate (and I think better fix).

alexpott’s picture

Ignore #93 that was one of the many solutions I considered... I think this patch is better because it copies from just above in the same install_run_task() method. Yes it would nice to fix the @todo

      // Because Batch API now returns a JSON response for intermediary steps,
      // but the installer doesn't handle Response objects yet, just send the
      // output here and emulate the old model.
      // @todo Replace this when we refactor the installer to use a request-
      //   response workflow.

But I don't think that this issue should be responsible for doing that even if it is the cause for the installer session weirdness.

The last submitted patch, 93: 2238561-3-93.patch, failed testing. View results

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

andypost’s picture

Version: 8.9.x-dev » 9.1.x-dev
alexpott’s picture

  1. +++ b/core/lib/Drupal/Core/Session/MetadataBag.php
    @@ -51,7 +51,8 @@ public function getCsrfTokenSeed() {
       /**
        * Clear the CSRF token seed.
        */
    

    Should be {@inheritdoc}

  2. +++ b/core/lib/Drupal/Core/TempStore/SharedTempStoreFactory.php
    @@ -48,13 +65,38 @@ class SharedTempStoreFactory {
    +      @trigger_error(__CLASS__ . '::__construct() now requires the current_user and session services to be injected. See https://www.drupal.org/node/3006268', E_USER_DEPRECATED);
    

    We need to update the deprecation format.

znerol’s picture

Issue tags: +Needs reroll
znerol’s picture

<?php
--- a/core/lib/Drupal/Core/Session/MetadataBag.php
+++ b/core/lib/Drupal/Core/Session/MetadataBag.php
@@ -51,7 +51,8 @@ public function getCsrfTokenSeed() {
   /**
    * Clear the CSRF token seed.
    */
-  public function clearCsrfTokenSeed() {
+  public function stampNew($lifetime = NULL) {
+    parent::stampNew($lifetime);
     unset($this->meta[static::CSRF_TOKEN_SEED]);
   }
?>

Use {@inheritdoc}.

<?php
--- a/core/lib/Drupal/Core/TempStore/SharedTempStore.php
+++ b/core/lib/Drupal/Core/TempStore/SharedTempStore.php
@@ -150,7 +172,11 @@ public function setIfNotExists($key, $value) {
       'data' => $value,
       'updated' => (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME'),
     ];
-    return $this->storage->setWithExpireIfNotExists($key, $value, $this->expire);
+    $set = $this->storage->setWithExpireIfNotExists($key, $value, $this->expire);
+    if ($set) {
+      $this->ensureAnonymousSession();
+    }
+    return $set;
   }
?>

Return value of KeyValueStoreExpirableInterface::setWithExpireIfNotExists() is TRUE if the data was set, or FALSE if it already existed. I do not see the point of calling ensureAnonymousSession() conditionally. And...

<?php 
   /**
@@ -208,6 +234,7 @@ public function set($key, $value) {
       'data' => $value,
       'updated' => (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME'),
     ];
+    $this->ensureAnonymousSession();
     $this->storage->setWithExpire($key, $value, $this->expire);
     $this->lockBackend->release($key);
   }

?>

...unless we have strong reasons against, I suggest to use the same sequence of calls for both flavors of set. First: ensureAnonymousSession(), second storage->setWithExpire....

<?php
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -1431,10 +1433,6 @@ function user_logout() {
   \Drupal::moduleHandler()->invokeAll('user_logout', [$user]);
 
   // Destroy the current session, and reset $user to the anonymous user.
-  // Note: In Symfony the session is intended to be destroyed with
-  // Session::invalidate(). Regrettably this method is currently broken and may
-  // lead to the creation of spurious session records in the database.
-  // @see https://github.com/symfony/symfony/issues/12375
   \Drupal::service('session_manager')->destroy();
   $user->setAccount(new AnonymousUserSession());
 }
?>

Do not remove this comment without updating the code. If we still cannot use Session::invalidate() for other reasons, then let's update the comment accordingly.

znerol’s picture

<?php
--- a/core/lib/Drupal/Core/TempStore/SharedTempStoreFactory.php
+++ b/core/lib/Drupal/Core/TempStore/SharedTempStoreFactory.php
@@ -32,6 +35,20 @@ class SharedTempStoreFactory {
    */
   protected $requestStack;
 
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountProxyInterface
+   */
+  protected $currentUser;
+
+  /**
+   * The session service.
+   *
+   * @var \Symfony\Component\HttpFoundation\Session\SessionInterface
+   */
+  protected $session;
+
?>

Please do not save the session in the instance. Always retrieve it from a request object.

<?php
@@ -76,12 +118,23 @@ public function get($collection, $owner = NULL) {
     // Use the currently authenticated user ID or the active user ID unless
     // the owner is overridden.
     if (!isset($owner)) {
-      $owner = \Drupal::currentUser()->id() ?: session_id();
+      $owner = $this->currentUser->id();
+      if ($this->currentUser->isAnonymous()) {
+        $has_session = $this->requestStack
+          ->getCurrentRequest()
+          ->hasSession();
+        if (!$has_session) {
+          $this->requestStack->getCurrentRequest()->setSession($this->session);
+          $this->session->start();
+        }
+        // Anonymous users store a random identifier in session.
+        $owner = $this->requestStack->getCurrentRequest()->getSession()->get('core.tempstore.shared.owner', Crypt::randomBytesBase64());
+      }
     }
?>

There seems to be a misunderstanding on how sessions work. We actually do inject a session object into every request in Drupal\Core\StackMiddleware\Session::handle() -- except on the command line.

If we need the tempstore on cli requests, then we need a fallback. Otherwise we could just disable it if neither there is a logged in user nor a session.

znerol’s picture

Status: Needs review » Needs work
ridhimaabrol24’s picture

Status: Needs work » Needs review
FileSize
20.62 KB

Patch uploaded for Drupal 9.1

Status: Needs review » Needs work

The last submitted patch, 103: 2238561-103.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

jofitz’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
13.22 KB
0 bytes

Rerolled patch from #94 for D9.1.x
(included interdiff against patch from #103 for reference)

ridhimaabrol24’s picture

Fixed 2 failing test cases in #103
Still looking for solution on the remaining one.

Status: Needs review » Needs work

The last submitted patch, 106: 2238561-106.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Hardik_Patel_12’s picture

Assigned: Unassigned » Hardik_Patel_12

working on it.

Hardik_Patel_12’s picture

Assigned: Hardik_Patel_12 » Unassigned

busy with other issue

eiriksm’s picture

Assigned: Unassigned » eiriksm

Trying to fix it up

eiriksm’s picture

Status: Needs work » Needs review
FileSize
20.36 KB
869 bytes

This fixes the last test for me locally.

eiriksm’s picture

Assigned: eiriksm » Unassigned
alexpott’s picture

@znerol thanks for the review. That seems quite simple to address - I think I was being a bit defensive there. But you're right we shouldn't need to start or set a session there.

Status: Needs review » Needs work

The last submitted patch, 113: 2238561-9.1.x-113.patch, failed testing. View results

alexpott’s picture

Status: Needs work » Needs review
FileSize
596 bytes
19.72 KB

Whoops

Status: Needs review » Needs work

The last submitted patch, 115: 2238561-9.1.x-115.patch, failed testing. View results

alexpott’s picture

Status: Needs work » Needs review
FileSize
802 bytes
19.87 KB

And one last thing. If the request doesn't have a session (because we're in CLI) - then there is no need to set the owner there.

amateescu’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/TempStore/SharedTempStoreFactory.php
@@ -32,6 +34,20 @@ class SharedTempStoreFactory {
+  protected $session;

This is not needed anymore.

And I think #100 hasn't been addressed yet :)

alexpott’s picture

Status: Needs work » Needs review
FileSize
4.97 KB
21.28 KB

#100 was addressed by #103 - no interdiff unfortuantely.

Patch addresses #118 and adds a test for the legacy mode of SharedTempStoreFactory::construct() - and changes the deprecation messages to meet the standard.

amateescu’s picture

Status: Needs review » Reviewed & tested by the community

Oh, right, I took a closer look at the patch and those remarks were indeed addressed. I think looks good to go now.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 119: 2238561-9.1.x-119.patch, failed testing. View results

alexpott’s picture

Status: Needs work » Reviewed & tested by the community

Random JS test fail.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review

@catch asked

do we need to be concerned that contrib might have to make similar changes to tempstore?

The most obvious module to have a look at this with is the flag module - which has the anonymous flagging capability. And indeed with this patch applied its tests/src/Functional/AnonymousFlagTest.php test fails.

Maybe that makes this Drupal 10 only because we need to make this change but doing it without breaking BC in terms of expectations around session naming is hard - maybe, in fact possible. Going to set to needs review and maybe I'll get so time to delve into this and see what changes it would mean for the flag module.

neclimdul’s picture

Was looking over the patch to see if I had any ideas on the BC question and I noticed something.

+++ b/core/lib/Drupal/Core/TempStore/PrivateTempStore.php
@@ -119,17 +120,16 @@ public function get($key) {
-      $this->requestStack
-        ->getCurrentRequest()
-        ->getSession()
-        ->set('core.tempstore.private', TRUE);
+      $session = $this->requestStack->getCurrentRequest()->getSession();
+      if (!$session->has('core.tempstore.private.owner')) {
+        $session->set('core.tempstore.private.owner', Crypt::randomBytesBase64());
+      }

@@ -224,8 +224,10 @@ protected function createkey($key) {
+      // Check to see if an owner key exists in the session.
       $this->startSession();
-      $owner = $this->requestStack->getCurrentRequest()->getSession()->getId();
+      $session = $this->requestStack->getCurrentRequest()->getSession();
+      $owner = $session->get('core.tempstore.private.owner');

On the surface this looks identical. I think overall the logic is fine but but there is _tiny_ and maybe important difference here. Previously we wrote the randomness to 'sid' on the session table which is a key and so has a guarantee of uniqueness between users. Now its a a property on the sessions which has no similar guarantees.

In practice, this randomness has a incredibly high probability of being unique, that is the point, so leaking across tempstores seems unlikely. But if we're confident in the cryptographic uniqueness of php's session id generation, why don't we just keep using the session id for the owner?

alexpott’s picture

I've thought about #124 too. I think this gets to one of our problems with starting sessions for anonymous users. I think our lazy sessions mean that we don't get a session ID until the end of the request and the session is possibly written. So whilst clashes should be rare and the data with be secured by the session name that is assigned on write. I think that we some way of forcibly starting a session and generating a session ID that can be used for things like this

I also think the updates for Symfony 4 to use PHP's SessionUpdateTimestampHandlerInterface might mean that we can remove more of custom code and rely on new PHP interfaces and Symfony code.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

mxr576’s picture

Should we join the effort and resolve these two at once?

alexpott’s picture

@mxr576 I think we should try to resolve the final issue here and then see what is left. The issue here feels well scoped whereas the other issue still needs more definition and I think will be easier once this has landed.

mxr576’s picture

I think will be easier once this has landed.

Definitely, I saw an overlap between this and the other issue, this was the reason why I asked. Applying the latest patch here is the first step to resolve the other issue.

dawehner’s picture

Here is a quick reroll against 9.2.x

The most obvious module to have a look at this with is the flag module - which has the anonymous flagging capability. And indeed with this patch applied its tests/src/Functional/AnonymousFlagTest.php test fails.

Looking into this some more:

  • Previously ->getId() always returned a value once ->start() had been called once at least.
  • Now as intended, getId() no longer returns a session ID necessarily.

    The calls made my flag module are:

    • \Drupal\flag\FlagService::ensureSession setting $_SESSION['flag'] = TRUE;
    • $sesion_manager->start()
    • In \Drupal\flag\FlagService::populateFlaggerDefaults it calls $sesion_manager->getId() and expects a session ID to return

I was interested how many other contrib modules call session_id(), some especially in the context of anonymous users, see http://grep.xnddx.ru/search?text=session_id%28%29&filename=

Status: Needs review » Needs work

The last submitted patch, 130: 2238561-130.patch, failed testing. View results

dawehner’s picture

Status: Needs work » Needs review
FileSize
21.68 KB
4.73 KB

This just resolves the test failures.

dawehner’s picture

FileSize
21.23 KB
4.03 KB

The last comment accidentally some experiment.

dawehner’s picture

What do you think about doing a smaller step forward, and still use something like

      // Randomly generate a session identifier for this request. This is
      // necessary because \Drupal\Core\TempStore\SharedTempStoreFactory::get()
      // wants to know the future session ID of a lazily started session in
      // advance.
      //
      // @todo: With current versions of PHP there is little reason to generate
      //   the session id from within application code. Consider using the
      //   default php session id instead of generating a custom one:
      //   https://www.drupal.org/node/2238561
      $this->setId(Crypt::randomBytesBase64());

for anonymous users.

In detail we could move this logic to getId() and just trigger it, if its value isn't set yet.
If this is hit, we know there is some deprecated code, and we could recommend something a long the lines of what @znerol
talks about in #2865991: provide an API to forcibly start a session. Then in Drupal 10 we could remove the artificial created ID.

alexpott’s picture

@dawehner thanks for working on this! I was just writing the following...

Reading #130 maybe that offers us a way forward. Maybe for BC we can set the session ID on getId if it is null at the point - or we can properly start a session so there is an ID.

... great minds :)

dawehner’s picture

A couple of further improvements:

  1. Undid the removal of clearCsrfTokenSeed, added a deprecation and a change record
  2. Implemented the suggestion to still generate a session ID for anonymous users, but throw a deprecation message

Here is a picture which proves that flag module now succeeds.
Shows a successful flag test

alexpott’s picture

Status: Needs review » Needs work
  1. Nice that this works. yay
  2. +++ b/core/lib/Drupal/Core/Session/MetadataBag.php
    @@ -56,4 +56,13 @@ public function stampNew($lifetime = NULL) {
    +    @trigger_error('Calling ' . __METHOD__ . '() is deprecated in drupal:9.2.0 and will be removed in drupal:10.0.0. Use $this->clearCsrfTokenSeed instead. See https://www.drupal.org/node/3187914', E_USER_DEPRECATED);
    +    unset($this->meta[static::CSRF_TOKEN_SEED]);
    

    $this->stampNew() is the replacement kinda. I'm unsure why anything would really need to do this. I think the code in http://codcontrib.hank.vps-private.net/node/31039526 should be encouraged to set and new seed rather than clearing. So they can avoid #2941102: Form has become outdated after login (CSRF token race condition).

    So I think this text should be Calling ' . __METHOD__ . '() is deprecated in drupal:9.2.0 and will be removed in drupal:10.0.0. There is no direct replacement. If code needs to invalidate the current CSRF token seed use \Drupal\Core\Session\MetadataBag::setCsrfTokenSeed(). See https://www.drupal.org/node/3187914

  3. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -134,6 +134,25 @@ public function start() {
    +    // Some code might rely on the existence of a session ID, before we even had
    +    // a real session, which was supported by previous versions.
    +    // We generate a random session ID here, but also throw a deprecation
    +    // as you should never get to this point.
    

    I think this can be improved. ... maybe something like

        if (empty($id)) {
          // Legacy code might rely on the existence of a session ID before a real
          // session exists. In this case, generate a random session ID to provide
          // backwards compatibility.
          @trigger_error('Calling ' . __METHOD__ . '() outside of an actual existing session is deprecated in drupal:9.2.0 and will be removed in drupal:10.0.0. This is often used for anonymous users. See https://www.drupal.org/node/3006306', E_USER_DEPRECATED);
          $this->setId(Crypt::randomBytesBase64());
        }
    

    There's a bit less repetition.

  4. +++ b/core/tests/Drupal/Tests/Core/Session/SessionManagerTest.php
    @@ -0,0 +1,54 @@
    +
    +class SessionManagerTest extends UnitTestCase {
    

    Add @covers.

znerol’s picture

In detail we could move this logic to getId() and just trigger it, if its value isn't set yet. If this is hit, we know there is some deprecated code, and we could recommend something a long the lines of what @znerol talks about in #2865991: provide an API to forcibly start a session. Then in Drupal 10 we could remove the artificial created ID.

Fully agree.

diff --git a/core/core.services.yml b/core/core.services.yml
index 5d01d87667..2b2212a3da 100644
--- a/core/lib/Drupal/Core/Session/MetadataBag.php
+++ b/core/lib/Drupal/Core/Session/MetadataBag.php

   /**
    * Clear the CSRF token seed.
    */
   public function clearCsrfTokenSeed() {
+    @trigger_error('Calling ' . __METHOD__ . '() is deprecated in drupal:9.2.0 and will be removed in drupal:10.0.0. Use $this->clearCsrfTokenSeed instead. See https://www.drupal.org/node/3187914', E_USER_DEPRECATED);
     unset($this->meta[static::CSRF_TOKEN_SEED]);
   }

According to the change record the suggestion is wrong, isn't it? Should be:

Use $this->stampNew instead.

alexpott’s picture

Status: Needs work » Needs review
FileSize
5.65 KB
26.19 KB

Fixing up #137 and #138. Also in light of #2941102: Form has become outdated after login (CSRF token race condition) I've changed stampNew() regenerated the CRSF seed. This highlights that often, because of lazy sessions, \Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage::regenerate() does not actually do anything because the PHP session is not actually started.

Status: Needs review » Needs work

The last submitted patch, 139: 2238561-4-139.patch, failed testing. View results

dawehner’s picture

Status: Needs work » Needs review
FileSize
26.2 KB
936 bytes

Fixing up #137 and #138. Also in light of #2941102: Form has become outdated after login (CSRF token race condition) I've changed stampNew() regenerated the CRSF seed. This highlights that often, because of lazy sessions, \Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage::regenerate() does not actually do anything because the PHP session is not actually started.

That makes sense for me.

This patch just fixes the wrong namespace of the two new test classes.

Status: Needs review » Needs work

The last submitted patch, 141: 2238561-141.patch, failed testing. View results

znerol’s picture

diff --git a/core/lib/Drupal/Core/Session/SessionManager.php b/core/lib/Drupal/Core/Session/SessionManager.php
index 42a9d8f273..fc07231441 100644
--- a/core/lib/Drupal/Core/Session/SessionManager.php
+++ b/core/lib/Drupal/Core/Session/SessionManager.php
@@ -119,17 +119,6 @@ public function start() {
+  /**
+   * {@inheritdoc}
+   */
+  public function getId() {
+    $id = $this->saveHandler->getId();
+
+  [...]
+    return $this->saveHandler->getId();
+  }

Please use parent::getId() instead of $this->saveHandler->getId(). That way we hopefully remember to remove the whole override in Drupal 10.

alexpott’s picture

Status: Needs work » Needs review
FileSize
1.08 KB
26.19 KB

@znerol good point re calling the parent. Made that change and also made it call the parent one time less for the usual use-case. Re removing in D10 - that's the going thing about adding the deprecation - it means we'll have to look at this code in order to get D10 ready :).

Status: Needs review » Needs work

The last submitted patch, 144: 2238561-4-144.patch, failed testing. View results

znerol’s picture

Tested this manually by clicking through the installer. Also added the flag module, configured it for anonymous users. During all the experiments watched the sessions table (mysql -e "select * from sessions;" $MYDB). Also eyed my browser cookies.

  1. Installer creates a session during the batch. DB entry and session cookie is created at the beginning, removed at the end: expected
  2. Login/Logout.DB entry and session cookie is created at login, removed at logout: expected
  3. Toggle flag as anonymous user. DB entry and session cookie is created: expected. Then login. Session id is regenerated in DB as well as the cookie value.

So far so good. However, the session-ids generated by PHP are shorter than the ones generated by Crypt::randomBytesBase64. On my buster system, the default value of the relevant session.sid_length is 22. Thus it wouldn't hurt if we specify a bigger value explicitly in default.services.yml. Guess something like this would do it:

parameters:
  session.storage.options:
    [...]
    # Specify the length of session ID string. Session ID length can be between
    # 22 to 256. The default is 52.
    # @default 52
    sid_length: 52
    #
    # Specify the number of bits in encoded session ID character. The possible
    # values are '4' (0-9, a-f), '5' (0-9, a-v), and '6' (0-9, a-z, A-Z, "-",
    # ","). The default is 5.
    # @default 5
    sid_bits_per_character: 5

Those parameters need to be reflected in core/core.services.yml and core/assets/scaffold/files/default.services.yml as well.

Note on the chosen parameters: Crypt::randomBytesBase64 generates 32 random bytes and encodes them using base64 resulting in a 43 character string (32*8/6 = 42.6). The sid_length parameter affects the length of the encoded string. With sid_bits_per_character=5 we need 8 bytes to encode five random bytes 32*8/5 = 51.2.

znerol’s picture

Note on the chosen parameters: Crypt::randomBytesBase64 generates 32 random bytes and encodes them using base64 resulting in a 43 character string (32*8/6 = 42.6). The sid_length parameter affects the length of the encoded string. With sid_bits_per_character=5 we need 8 bytes to encode five random bytes 32*8/5 = 51.2.

According to PHP session security page, the following values are recommended:

sid_length: 48
sid_bits_per_character: 6

We should just use those values IMHO. Like this we get 36 bytes of random values.

alexpott’s picture

Status: Needs work » Needs review
FileSize
4.53 KB
30.57 KB

@znerol great catch. Here's an implementation of that. I add the setting in \Drupal\Core\Session\SessionConfiguration::__construct() as well to ensure they are set to the defaults because the defaults from core.services.yml are not merged in.

znerol’s picture

Testing this again manually and found another behavioral change:

  1. When changing the password from within the profile page, the session is regenerated. Without the patch applied, the old session is removed from storage. With the patch applied the old session remains in storage and an additional one is created.

In the current version of SessionManager::regenerate(), the $destroy and $lifetime parameters are unsupported because parent::regenerate() was never called. However, the current implementation behaves as if $destroy was set to TRUE. The patch now passes the $destroy parameter directly to parent::regenerate(). Unfortunately, the $destroy parameter defaults to FALSE.

As a result it is necessary to update all calls to Session::migrate in order to restore the current behavior (option 1).

Alternatively we could keep the old behavior by explicitly calling parent::regenerate(TRUE). In that case we'd also keep the throw statement and leave all calls to Session::migrate unchanged (option 2).

I personally would prefer the second option. I do not see any use case where it would be desirable to keep around old sessions in the database after migrating session data to a new/regenerated session id. In that light, I guess that defaulting to $destroy=false in both, the Session as well as the SessionStorage interface was probably a mistake in Symfony inspired from weird PHP docs. More trustworthy sources, .e.g. OWASP session management cheat sheet mention explicitly that session_regenerate_id(true) should be used.

alexpott’s picture

@znerol really nice catch. I've added test coverage of this behaviour and come up with a solution along the lines of option 2 that preserves the existing behaviour but also supports the ability to migrate without destroying so we obey the interface if someone passes in $destroy = FALSE. I did consider changing the default value in the signature to TRUE but I think this way might be better. Not sure. Tricky.

The last submitted patch, 150: 2238561-4-150-test-only.patch, failed testing. View results

znerol’s picture

+++ b/core/lib/Drupal/Core/Session/SessionManager.php
@@ -210,34 +216,23 @@ public function regenerate($destroy = FALSE, $lifetime = NULL) {
+    if (count(func_get_args())) {
+      $destroy = TRUE;
     }

I believe that count(func_get_args()) will always return 2 and hence this condition will never evaluate to false. SessionManager::regenerate isn't (and shouldn't be) called directly. Core and contrib code is supposed to use the symfony session retrieved from the request and call Session::migrate(). So I'd expect that in the usual case SessionManager::regenerate will be invoked with both parameters specified (i.e., $destroy=false, $lifetime=null).

alexpott’s picture

@znerol oops... I thought I had added === 0 .... lol. If I had it would have been broken :)

znerol’s picture

  1. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -210,34 +216,21 @@ public function regenerate($destroy = FALSE, $lifetime = NULL) {
    +    if (\PHP_SESSION_ACTIVE !== session_status()) {
    +      // Ensure the metadata bag has been stamped. If the parent::regenerate()
    +      // is called prior to the session being started it will not refresh the
    +      // metadata as expected.
    +      $this->getMetadataBag()->stampNew($lifetime);
    +      return FALSE;
    

    So much better than the previous version!

  2. +++ b/core/modules/user/user.module
    @@ -460,8 +460,10 @@ function user_login_finalize(UserInterface $account) {
    -  \Drupal::service('session')->migrate();
    -  \Drupal::service('session')->set('uid', $account->id());
    +  /** @var \Symfony\Component\HttpFoundation\Session\Session $session */
    +  $session = \Drupal::service('session');
    +  $session->migrate(TRUE);
    +  $session->set('uid', $account->id());
       \Drupal::moduleHandler()->invokeAll('user_login', [$account]);
    

    We can leave out this hunk (or at least remove the $destroy=TRUE param from the migrate call.

alexpott’s picture

I was wondering about #154.2 too. I think reverting this change is practical. Shows we don't need to change it :)

So the last thing to clear up here is #124 - which makes the point that we can't be sure of the uniqueness of the tempstore owner key.

In practice, this randomness has a incredibly high probability of being unique, that is the point, so leaking across tempstores seems unlikely. But if we're confident in the cryptographic uniqueness of php's session id generation, why don't we just keep using the session id for the owner?

To answer this point - we can't get the session ID until the session is properly started. So at the point where we create a the tempstore for anonymous user we don't have a session ID. Given that session IDs prior to this patch were created via the same Crypt::randomBytesBase64() I think there's no real change in uniqueness here.

znerol’s picture

Issue summary: View changes
znerol’s picture

Issue summary: View changes
znerol’s picture

Status: Needs review » Reviewed & tested by the community

Took this patch to one last manual test drive and reread the patch one more time. All lights green!

This was last RTBC in #120. The major changes since then are:

  1. Backwards compatibility for modules which use the session id as identifier of anonymous users (e.g. flag).
  2. Sane values for PHP ini settings session.sid_length and session.sid_bits_per_character

The work on this issue revealed that SessionManager::regenerate does not fully respect the contract of SessionStorageInterface::regenerate. This is due to the fact that Drupal historically had no way to renew the session id without destroying the old database record. Since the Drupal behavior is inline with OWASP session management recommendations, continuing to enforce $destroy = TRUE is the best way forward.

neclimdul’s picture

Didn't get through a really thorough review but that's probably fine, lot of good eyes on this. Did notice this little thing though.

+++ b/core/lib/Drupal/Core/TempStore/SharedTempStoreFactory.php
@@ -48,13 +57,29 @@ class SharedTempStoreFactory {
+    if ($current_user instanceof AccountProxyInterface) {
+      $this->currentUser = $current_user;
+    }
+    else {
+      $this->currentUser = \Drupal::currentUser();
+    }
+    if (!($current_user instanceof AccountProxyInterface)) {
+      @trigger_error('Calling ' . __METHOD__ . '() without the $current_user argument is deprecated in drupal:9.2.0 and will be required in drupal:10.0.0. See https://www.drupal.org/node/3006268', E_USER_DEPRECATED);

Isn't the second if just the same as the else and can be simplified?

alexpott’s picture

@neclimdul good point. We can move this about so the deprecation removal only needs to remove whole lines of code which is nicer. Leaving at RTBC because nothing is really changing. The patch already has test coverage of these deprecations.

alexpott’s picture

+++ b/core/lib/Drupal/Core/Session/SessionManager.php
@@ -332,18 +331,4 @@ protected function getSessionDataMask() {
-  /**
-   * Migrates the current session to a new session id.
-   *
-   * @param string $old_session_id
-   *   The old session ID. The new session ID is $this->getId().
-   */
-  protected function migrateStoredSession($old_session_id) {
-    $fields = ['sid' => Crypt::hashBase64($this->getId())];
-    $this->connection->update('sessions')
-      ->fields($fields)
-      ->condition('sid', Crypt::hashBase64($old_session_id))
-      ->execute();
-  }

We could argue that removing this is a API change. There are no usages in contrib (unsurprising) and I think that removing this is better than leaving it in a deprecating.

If you're code extending SessionManager and call this then you will need to make changes due to this change. There is one example of extending SessionManager in contrib - it's http://codcontrib.hank.vps-private.net/node/30885709 - which is interesting because as we get closer to Symfony's session code we get closer to supporting samesite cookies for session out-of-the-box.

neclimdul’s picture

Hah, that's an even better approach then I was thinking. Only one code block, nice work.

So I was going to say "that looks like it is a really internal sort of method." But then I looked at the history and it got more complicated. It was _specifically_ added to abstract that database call to simplify overriding a session manager. #2371875: session_manager can't be reasonably overridden

I think I'm still going to argue this is an internal method though. The intent of this part of the issue was to centralize the database logic for migrating a session from insecure to a secure session. When we removed mixed mode sessions this we completely trashed this "interface" and it arguably became an internal method because this no-longer migrates anything. Its just a helper to regenerate the id.

Not sure though, its a good observation.

catch’s picture

Status: Reviewed & tested by the community » Needs work

A couple of dozen or more comments behind on this one so had to catch up a bit. Mostly very minor points found reviewing:

  1. +++ b/core/assets/scaffold/files/default.services.yml
    @@ -36,6 +36,18 @@ parameters:
         # cookie_domain: '.example.com'
         #
    +    # Specify the length of session ID string. Session ID length can be between
    +    # 22 to 256. The PHP recommended value is 48. See
    

    Nit: 'Specify the session ID string length'. Or 'Specify the length of the session ID string'.

  2. +++ b/core/assets/scaffold/files/default.services.yml
    @@ -36,6 +36,18 @@ parameters:
    +    #
    +    # Specify the number of bits in encoded session ID character. The possible
    +    # values are '4' (0-9, a-f), '5' (0-9, a-v), and '6' (0-9, a-z, A-Z, "-",
    +    # ","). The PHP recommended value is 5. See
    +    # https://www.php.net/manual/session.security.ini.php for more information.
    +    # @default 6
    +    sid_bits_per_character: 6
       twig.config:
    

    Can we explain why we're using 6 bits per character instead of the recommended 5? Even if it's just to match what we were using before.

  3. +++ b/core/lib/Drupal/Core/Session/MetadataBag.php
    @@ -48,10 +49,22 @@ public function getCsrfTokenSeed() {
    +
    +    // We set token seed immediately to avoid race condition between two
    +    // simultaneous requests without a seed.
    +    $this->setCsrfTokenSeed(Crypt::randomBytesBase64());
    

    Nit: 'set the token seed immediately to avoid a race condition'

    Or 'set the token seed immediately to avoid race conditions'

  4. +++ b/core/lib/Drupal/Core/Session/SessionConfiguration.php
    @@ -22,9 +22,12 @@ class SessionConfiguration implements SessionConfigurationInterface {
    -    $this->options = $options;
    +    // Provide sensible defaults for sid_length and sid_bits_per_character.
    +    // See core/assets/scaffold/files/default.services.yml for more information.
    +    $this->options = $options + ['sid_length' => 48, 'sid_bits_per_character' => 6];
       }
     
    

    Is there no way to get these from the container? If we change them in core.services.yml/default.services.yml they'll also need to be kept in sync here.

    If not, can we add a comment to default.services.yml about needing to keep them in sync?

  5. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -210,34 +216,21 @@ public function regenerate($destroy = FALSE, $lifetime = NULL) {
     
    -    // We do not support the optional $destroy and $lifetime parameters as long
    -    // as #2238561 remains open.
    -    if ($destroy || isset($lifetime)) {
    

    Rare case of removing a @todo for an issue in the actual issue referenced ;)

  6. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -210,34 +216,21 @@ public function regenerate($destroy = FALSE, $lifetime = NULL) {
    -      setcookie($this->getName(), $this->getId(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
    -      $this->migrateStoredSession($old_session_id);
    +    // Drupal will always destroy the existing session when regenerating a
    +    // session. This is inline with the recommendations of @link https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#renew-the-session-id-after-any-privilege-level-change
    +    // OWASP session management cheat sheet. @endlink
    +    $destroy = TRUE;
    +
    

    Do we need to have some kind of warning (assert? trigger_error()?) when $destroy is FALSE given it's not supported here?

  7. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -332,18 +331,4 @@ protected function getSessionDataMask() {
     
    -  /**
    -   * Migrates the current session to a new session id.
    -   *
    -   * @param string $old_session_id
    -   *   The old session ID. The new session ID is $this->getId().
    -   */
    -  protected function migrateStoredSession($old_session_id) {
    

    I think this probably OK to remove as dead code per the last few comments in this issue - should be in the change record though just in case.

  8. +++ b/core/lib/Drupal/Core/TempStore/SharedTempStore.php
    @@ -150,7 +170,9 @@ public function setIfNotExists($key, $value) {
         ];
    -    return $this->storage->setWithExpireIfNotExists($key, $value, $this->expire);
    +    $this->ensureAnonymousSession();
    +    $set = $this->storage->setWithExpireIfNotExists($key, $value, $this->expire);
    +    return $set;
    

    OK so this shows the changes that are necessary in contrib, but we have a bc layer so they're only done here to avoid deprecation messages. All looks OK.

  9. +++ b/core/lib/Drupal/Core/TempStore/SharedTempStore.php
    @@ -279,4 +302,17 @@ public function deleteIfOwner($key) {
     
    +  /**
    +   * Stores the owner in session if the user is anonymous.
    +   *
    

    nit: the session

  10. +++ b/core/lib/Drupal/Core/TempStore/SharedTempStore.php
    @@ -279,4 +302,17 @@ public function deleteIfOwner($key) {
    +   */
    +  protected function ensureAnonymousSession() {
    +    // If this is being run from the CLI then the request will not have
    +    // session.
    

    nit: will not have a session.

  11. +++ b/core/lib/Drupal/Core/TempStore/SharedTempStoreFactory.php
    @@ -76,12 +97,20 @@ public function get($collection, $owner = NULL) {
    +        $owner = Crypt::randomBytesBase64();
    +        if ($this->requestStack->getCurrentRequest()->hasSession()) {
    +          // Anonymous users store a random identifier in session if session is
    +          // available.
    

    nit: Store a random identifier for anonymous users if the session is available. - the anonymous user isn't storing anything.

  12. +++ b/core/modules/user/tests/src/Functional/UserEditTest.php
    @@ -79,6 +79,11 @@ public function testUserEdit() {
     
    +    // Confirm there's only one session in the database as the existing session
    +    // has been migrated when the password is changed.
    +    // @see \Drupal\user\Entity\User::postSave()
    +    $this->assertSame(1, (int) \Drupal::database()->select('sessions', 's')->countQuery()->execute()->fetchField());
    +
    

    Given we're adding test coverage to ensure the same behaviour as currently, could we have a test only patch to show it passes with the current code.

alexpott’s picture

Thanks for the review @catch. Re #163
1. Fixed
2. The recommended value is actually six - see https://www.php.net/manual/session.security.ini.php. Fixed that instead
3. Fixed
4. No there's not. I've thought about filing an issue to merge the core.services.yml paramaters in - but this is really tricky because its part of the session.storage.options parameter its not a whole value. See \Drupal\Core\DependencyInjection\YamlFileLoader::load(). Added
5. :)
6. I thought about adding an assert but it's not possible - for the same reasons that I was struggling around #152. For what it is worth this is not a change from the current behaviour. We don't respect this param currently.
7. I'll add a new separate CR. It doesn't feel that this fits on any of the others.
8. +1
9. Fixed
10. Fixed
11. Fixed
12. Sure attached.

alexpott’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community
Issue tags: +9.2.0 release notes

Given that all the changes in #164 were to documentation setting back to rtbc.

  • catch committed cda287d on 9.2.x
    Issue #2238561 by alexpott, dawehner, andypost, bradjones1,...
catch’s picture

Status: Reviewed & tested by the community » Fixed

All the docs changes look good.

Committed cda287d and pushed to 9.2.x. Thanks!

Wim Leers’s picture

AFAICT this should not affect https://www.drupal.org/project/big_pipe_sessionless because \Drupal\Core\Session\SessionConfigurationInterface::hasSession() implementations are not impacted.

This is also why this commit is not changing \Drupal\Core\Cache\Context\SessionCacheContext.

Can somebody confirm this? 🙏

alexpott’s picture

\Drupal\Core\Session\SessionConfigurationInterface::hasSession() is not affected - that's checking for the existence of the session cookie. The cookie is only set when the headers are sent and the next request made (afaik) so I concur https://www.drupal.org/project/big_pipe_sessionless should not be affected.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.