Background
- 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).
-
Drupal does not rely on built-in PHP methods for generating the session id currently, but uses a random 32 byte value.
-
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).
- 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
- Our approach does not allow us to use session_regenerate_id().
- 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
-
PHP 5.4 and subsequently PHP 7.1 ship with improvements regarding session ID generation:
- 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. - 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. - Using session.sid_length, the length of the generated session id can be specified
- With session.sid_bits_per_character it is possible to choose the alphabet for the generated session id
- The INI setting session.entropy_file defaults to
-
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.
- 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.
-
→ 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.
Comment | File | Size | Author |
---|---|---|---|
#164 | 2238561-4-164.patch | 32.68 KB | alexpott |
#164 | 2238561-4-user-edit-test-only.patch | 947 bytes | alexpott |
#164 | 160-164-interdiff.txt | 5.15 KB | alexpott |
#160 | 2238561-4-160.patch | 32.28 KB | alexpott |
#160 | 155-160-interdiff.txt | 2.62 KB | alexpott |
Comments
Comment #1
sunAdding some structure to the issue summary.
Comment #2
sunComment #3
moshe weitzman CreditAttribution: moshe weitzman commentedMakes sense to me.
Comment #4
sunThere 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?
Comment #5
pwolanin CreditAttribution: pwolanin commentedNo, 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.
Comment #6
pwolanin CreditAttribution: pwolanin commentedsorry, 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?
Comment #7
znerol CreditAttribution: znerol commentedComment #8
znerol CreditAttribution: znerol commentedComment #9
znerol CreditAttribution: znerol commentedOkay, I propose the following scripts for performance analyzes:
drupal_random_key()
(rkey.php
):session_regenerate_id()
(regen.php
):Then use
ab -n500 http://your-server/rkey.php
andab -n500 http://your-server/regen.php
My results:
Relevant PHP ini settings:
I had to manually set the hash function, such that
SHA-1
is used instead ofMD5
, the other settings are defaults on my system.PHP version:
PHP 5.4.27-1~dotdeb.1
Comment #10
znerol CreditAttribution: znerol commentedAlso 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()).Comment #11
drummBakery 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.Comment #12
pwolanin CreditAttribution: pwolanin commentedHmm, 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?
Comment #13
Damien Tournoud CreditAttribution: Damien Tournoud commentedAs 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.
Comment #14
znerol CreditAttribution: znerol commentedRe #11/#12: This is not about
session_name()
(= cookie name) but aboutsession_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()
.Comment #15
znerol CreditAttribution: znerol commentedComment #16
znerol CreditAttribution: znerol commentedComment #17
znerol CreditAttribution: znerol commentedComment #18
eric_a CreditAttribution: eric_a commentedComment #19
jibranBack to active again.
Comment #20
znerol CreditAttribution: znerol commentedThere 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 inDrupal\user\TempStoreFactory
. In order to eliminate thesession_id()
call there it is either necessary to make the$owner
parameter of theget()
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.Comment #21
znerol CreditAttribution: znerol commentedComment #22
znerol CreditAttribution: znerol commentedComment #23
jibranAll the related issues are fixed.
Comment #24
andypostAlso after #2421263: Potential data loss: concurrent (i.e. by different users) node edits leak through preview there's only
\Drupal\user\SharedTempStoreFactory::get()
usessession_id()
and\Drupal\user\PrivateTempStore::getOwner()
uses$this->requestStack->getCurrentRequest()->getSession()->getId()
so it makes sense to replace.Comment #25
andypostInitial code
PS: once Core require 5.5 we can implement own http://php.net/manual/en/sessionhandler.create-sid.php
Comment #27
xjmComment #28
znerol CreditAttribution: znerol commentedComment #29
andypostWIP - clean-up a bit, and add @todo to fix properly
PS:
Drupal\Tests\user\Unit\PrivateTempStoreTest
still brokenComment #31
znerol CreditAttribution: znerol commentedI think we should delegate that to the parent class. Also note that the switch from
session_id()
toregenerate_session_id()
will also allow us to remove almost all the other cruft from this method. For examplestartNow
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:It might even be possible to eliminate the
clearCsrfTokenSeed()
by simply overridingstampNew()
in the drupalSession\MetadataBag
.Comment #32
znerol CreditAttribution: znerol commentedOh, 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 overrideSessionHandler
.Comment #33
andypostIt 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?
Comment #34
znerol CreditAttribution: znerol commentedTest 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.Comment #35
andypostrevert removal of request stack, should fix some tests
working on #31
Comment #37
andypostAddressed #31, let's see...
PS: somehow installer tests are failed
Comment #39
cilefen CreditAttribution: cilefen commentedThis 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.
Comment #42
lainosantos CreditAttribution: lainosantos commentedHello everyone. Any news here?
Comment #43
andypostComment #45
skyredwangThe current session id generation allows the character "_" (underscore) in this function:
But, PHP session id doesn't allow this "_" character, otherwise, it would throw error:
I am trying to understand how the session ids generated by the current system (up to Core 8.3.2) is still working.
Comment #48
zessx CreditAttribution: zessx commentedThe offending class is
\Drupal\Core\Session\SessionManager
: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:Comment #50
volegerComment #51
jofitzRe-rolled.
Comment #53
alexpottTrying 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().
Comment #55
cosmicdreams CreditAttribution: cosmicdreams at Nerdery commentedTechnically, this can be injected as the 'current_user' service.
We're injecting the Request but we aren't injecting the session service? Perhaps we should.
We use the session service again here so if we had injected it we would be able to reuse it here.
Comment #56
neclimduls/drupap/drupal/
?Comment #57
alexpott1. 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.
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:
work for anonymous users. With this patch
$this->requestStack->getCurrentRequest()->getSession()->getId();
will return an empty string because$session->start();
no longer callssession_id(some random value)
and we do our lazy session thing.Comment #58
alexpottIf 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.Comment #60
alexpottLazy sessions and temp stores are such fun!
Comment #62
alexpottTemp stores are still fun :)
Comment #63
catchdrupap?
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.
Comment #64
alexpottThe 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.
Comment #65
alexpottI'm not convinced that \Drupal\Core\Cache\Context\SessionCacheContext will work the way we expect it to :(
Comment #66
alexpottLet's see.
Comment #68
andypostWhy 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
Comment #69
alexpott@andypost - no reason - I threw it in there as a hack to see if it would fix it. Changes to use $session.
Comment #70
alexpottFinished 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 withAnd I have wildly long session IDs in my browser and Drupal's session system is still working.
Comment #71
alexpottThe CRs are https://www.drupal.org/node/3006306 and https://www.drupal.org/node/3006268
Comment #72
catchShould 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.
Comment #73
alexpott@catch yep we can do that in
install_finished()
Comment #74
larowlanIs this still the case?
Comment #75
alexpott@larowlan yep this code for anonymous flags...
in
src/FlagService.php
is not really correct. Flag passes the session ID all over the place.Comment #76
andypostComment #77
kalyansamanta CreditAttribution: kalyansamanta at Asentech LLC commentedComment #78
kalyansamanta CreditAttribution: kalyansamanta at Asentech LLC commentedComment #79
tim.plunkettComment #80
lokapujyaCan remove the word "Consider" from the issue title.
Comment #81
andypostComment #83
darren ohComment #84
darren ohAs 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.
Comment #85
darren ohI 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.
Comment #86
alexpott@Darren Oh unfortunately the example code hints that it might not be a perfect fit for us...
That won't work with our lazy sessions where we don;t want to start a session unless 100% necessary.
Comment #87
darren ohWe 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().
Comment #88
alexpott@Darren Oh good point!
Comment #89
darren ohDid 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.
Comment #90
bradjones1Re-rolled for 8.8.x. Let's see what testbot says.
Comment #92
bradjones1The 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()` regardingSession::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 inSessionManager
we can explain why we are not usingSession::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.Comment #93
alexpottThe 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).
Comment #94
alexpottIgnore #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 @todoBut I don't think that this issue should be responsible for doing that even if it is the cause for the installer session weirdness.
Comment #97
andypostComment #98
alexpottShould be
{@inheritdoc}
We need to update the deprecation format.
Comment #99
znerol CreditAttribution: znerol commentedComment #100
znerol CreditAttribution: znerol commentedUse
{@inheritdoc}
.Return value of
KeyValueStoreExpirableInterface::setWithExpireIfNotExists()
isTRUE
if the data was set, orFALSE
if it already existed. I do not see the point of callingensureAnonymousSession()
conditionally. And......unless we have strong reasons against, I suggest to use the same sequence of calls for both flavors of
set
. First:ensureAnonymousSession()
, secondstorage->setWithExpire...
.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.Comment #101
znerol CreditAttribution: znerol commentedPlease do not save the session in the instance. Always retrieve it from a request object.
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.
Comment #102
znerol CreditAttribution: znerol commentedComment #103
ridhimaabrol24 CreditAttribution: ridhimaabrol24 at Material for Drupal India Association commentedPatch uploaded for Drupal 9.1
Comment #105
jofitzRerolled patch from #94 for D9.1.x
(included interdiff against patch from #103 for reference)
Comment #106
ridhimaabrol24 CreditAttribution: ridhimaabrol24 at Material for Drupal India Association commentedFixed 2 failing test cases in #103
Still looking for solution on the remaining one.
Comment #108
hardik_patel_12 CreditAttribution: hardik_patel_12 at QED42 commentedworking on it.
Comment #109
hardik_patel_12 CreditAttribution: hardik_patel_12 at QED42 commentedbusy with other issue
Comment #110
eiriksmTrying to fix it up
Comment #111
eiriksmThis fixes the last test for me locally.
Comment #112
eiriksmComment #113
alexpott@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.
Comment #115
alexpottWhoops
Comment #117
alexpottAnd 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.
Comment #118
amateescu CreditAttribution: amateescu as a volunteer commentedThis is not needed anymore.
And I think #100 hasn't been addressed yet :)
Comment #119
alexpott#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.
Comment #120
amateescu CreditAttribution: amateescu as a volunteer commentedOh, right, I took a closer look at the patch and those remarks were indeed addressed. I think looks good to go now.
Comment #122
alexpottRandom JS test fail.
Comment #123
alexpott@catch asked
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.
Comment #124
neclimdulWas looking over the patch to see if I had any ideas on the BC question and I noticed something.
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?
Comment #125
alexpottI'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.
Comment #127
mxr576Should we join the effort and resolve these two at once?
Comment #128
alexpott@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.
Comment #129
mxr576Definitely, 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.
Comment #130
dawehnerHere is a quick reroll against 9.2.x
Looking into this some more:
->getId()
always returned a value once->start()
had been called once at least.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()
\Drupal\flag\FlagService::populateFlaggerDefaults
it calls$sesion_manager->getId()
and expects a session ID to returnI 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=
Comment #132
dawehnerThis just resolves the test failures.
Comment #133
dawehnerThe last comment accidentally some experiment.
Comment #134
dawehnerWhat do you think about doing a smaller step forward, and still use something like
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.
Comment #135
alexpott@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 :)
Comment #136
dawehnerA couple of further improvements:
Here is a picture which proves that flag module now succeeds.
Comment #137
alexpott$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
I think this can be improved. ... maybe something like
There's a bit less repetition.
Add @covers.
Comment #138
znerol CreditAttribution: znerol commentedFully agree.
According to the change record the suggestion is wrong, isn't it? Should be:
Use $this->stampNew instead.
Comment #139
alexpottFixing 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.Comment #141
dawehnerThat makes sense for me.
This patch just fixes the wrong namespace of the two new test classes.
Comment #143
znerol CreditAttribution: znerol commentedPlease use
parent::getId()
instead of$this->saveHandler->getId()
. That way we hopefully remember to remove the whole override in Drupal 10.Comment #144
alexpott@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 :).
Comment #146
znerol CreditAttribution: znerol commentedTested 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.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 indefault.services.yml
. Guess something like this would do it:Those parameters need to be reflected in
core/core.services.yml
andcore/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
). Thesid_length
parameter affects the length of the encoded string. Withsid_bits_per_character=5
we need 8 bytes to encode five random bytes32*8/5 = 51.2
.Comment #147
znerol CreditAttribution: znerol commentedAccording to PHP session security page, the following values are recommended:
We should just use those values IMHO. Like this we get 36 bytes of random values.
Comment #148
alexpott@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.
Comment #149
znerol CreditAttribution: znerol commentedTesting this again manually and found another behavioral change:
In the current version of
SessionManager::regenerate()
, the$destroy
and$lifetime
parameters are unsupported becauseparent::regenerate()
was never called. However, the current implementation behaves as if$destroy
was set toTRUE
. The patch now passes the$destroy
parameter directly toparent::regenerate()
. Unfortunately, the$destroy
parameter defaults toFALSE
.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 thethrow
statement and leave all calls toSession::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 thatsession_regenerate_id(true)
should be used.Comment #150
alexpott@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.
Comment #152
znerol CreditAttribution: znerol commentedI believe that
count(func_get_args())
will always return2
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 caseSessionManager::regenerate
will be invoked with both parameters specified (i.e.,$destroy=false, $lifetime=null
).Comment #153
alexpott@znerol oops... I thought I had added === 0 .... lol. If I had it would have been broken :)
Comment #154
znerol CreditAttribution: znerol commentedSo much better than the previous version!
We can leave out this hunk (or at least remove the
$destroy=TRUE
param from themigrate
call.Comment #155
alexpottI 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.
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.Comment #156
znerol CreditAttribution: znerol commentedComment #157
znerol CreditAttribution: znerol commentedComment #158
znerol CreditAttribution: znerol commentedTook 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:
session.sid_length
andsession.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.Comment #159
neclimdulDidn't get through a really thorough review but that's probably fine, lot of good eyes on this. Did notice this little thing though.
Isn't the second if just the same as the else and can be simplified?
Comment #160
alexpott@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.
Comment #161
alexpottWe 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.
Comment #162
neclimdulHah, 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.
Comment #163
catchA couple of dozen or more comments behind on this one so had to catch up a bit. Mostly very minor points found reviewing:
Nit: 'Specify the session ID string length'. Or 'Specify the length of the session ID string'.
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.
Nit: 'set the token seed immediately to avoid a race condition'
Or 'set the token seed immediately to avoid race conditions'
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?
Rare case of removing a @todo for an issue in the actual issue referenced ;)
Do we need to have some kind of warning (assert? trigger_error()?) when $destroy is FALSE given it's not supported here?
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.
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.
nit: the session
nit: will not have a session.
nit: Store a random identifier for anonymous users if the session is available. - the anonymous user isn't storing anything.
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.
Comment #164
alexpottThanks 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()
. Added5. :)
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.
Comment #165
alexpottGiven that all the changes in #164 were to documentation setting back to rtbc.
Comment #167
catchAll the docs changes look good.
Committed cda287d and pushed to 9.2.x. Thanks!
Comment #168
wim leersAFAICT 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? 🙏
Comment #169
alexpott\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.