Problem/Motivation
When
- a user has never logged in and attempts to log in or
- users are primarily logging in via SSO. (In that case, they do not/may not have a password for local login.)
these PHP 8 warnings are a result:
Deprecated function: substr(): Passing null to parameter #1 ($string) of type string is deprecated in Drupal\Core\Password\PhpassHashedPassword->check() (line 223 of core/lib/Drupal/Core/Password/PhpassHashedPassword.php).
Deprecated function: substr(): Passing null to parameter #1 ($string) of type string is deprecated in Drupal\Core\Password\PhpassHashedPassword->check() (line 234 of core/lib/Drupal/Core/Password/PhpassHashedPassword.php).This is the reason:
Hint: the `pass`column in `users_field_data` database table is nullable
Function authenticate in UserAuth.php line 50 has this:
if ($this->passwordChecker->check($password, $account->getPassword())) {
If the user has never logged in, getPassword() returns null. That sends null to the $hash parameter in checkI() in PhpassHashedPassword.php. It then tries to send that to substr() causing the warning as passing null where it's expecting a string is deprecated.
Steps to reproduce
Attempt to log in with an account that has never logged in.
Proposed resolution
I'm thinking we can simply check if getPassword() returns null and skip out if it does. I think this is an edge case because the account isn't being created by the user so there is no password set. In that case, it wouldn't be possible for them to log in without requesting a password reset, anyway.
Remaining tasks
Make the patch (in progress).
User interface changes
API changes
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #82 | 3305807-82.patch | 3.83 KB | andypost |
Issue fork drupal-3305807
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
michelleHere's the patch. Sending in empty instead of null seems to work but it may be better to add more code and just avoid calling check() all together?
Comment #4
mediabounds commentedWe also encountered this issue when user accounts get created outside of the normal registration process (in our case, they're getting created via the External Authentication module).
Like Michelle suggested, since the documentation for
PasswordInterface::checkexpects to get a string for the hash, it seems we should avoid callingcheckaltogether.Attaching an updated patch(es) with a failing test and a fix.
Comment #5
mediabounds commentedForgot to include the test in the full patch. Updated patches.
Comment #6
mediabounds commentedFixing whitespace issues in previous patches.
Comment #8
mediabounds commentedComment #9
mxr576This could also happen when users are primarily logging in via SSO. In that case, they do not/may not have a password for local login.
(I do not want to go into details about which module is used for SSO because the fact that the `pass`column in `users_field_data` database table is nullable should justify why this issue should be fixed.)
Comment #10
larowlanThe fact we had to do this twice in core makes me think that there may be other instances in contrib that need to allow for this.
For that reason I think the fix should be in the password service rather than in the user module.
If we look at the ::check method we see it passes the second argument ($hash) to substr to check
a) if it is a legacy updated password (starts with U$'
b) is a d7 password (starts with $S$)
c) is a phpass/phpbb3 password (starts with '$P$' or '$H$'
If it is none of these it returns FALSE.
So I think the ::check method should check before it calls substr that $hash is a string and non-zero length. If it is neither of those, it can return FALSE early, as all of the substr tests will fail anyway, ending up in the default clause of the switch.
This test doesn't have a positive assert, so in theory the code path inside the service could change, and the test would keep passing because it may no longer call the method we're asserting should never be called.
I think this is moot anyway if we move the fix into the password check service.
So we should remove this test and update \Drupal\Tests\Core\Password\PasswordHashingTest to add a case where $hash passed is NULL. We can then add a positive assert (e.g. assertFalse($password->check($a_password, NULL)))
Comment #11
jwwj commentedI think larowlan makes a valid point. It's the password checker that uses a method which requires a non-null value, and there might be valid use cases for the User->getPassword() method to return null. IMO it would make sense to fix this issue in the password checker instead of in UserAuth or the User entity.
Comment #13
danchadwick commentedAlong the lines of #10, here's a patch for 9.5.2 for those users who need to move past this. My site generates all accounts programmatically, so all with null passwords. I have confirmed the patch with manual testing. This patch won't apply to D10 because of the function signature change to use the SensitiveParmeter attribute. I simply cast to string because the code body handles empty strings cleanly and quickly and the code is simpler and more concise.
Comment #14
andypostA bit of clean-up for #6, hope this will address feedback on #10.1
Comment #15
andypostprobably it needs follow-up for the interdiff because changing interface
Comment #16
andypostEven if user created by drush it still can have null password, so we should change interface
Comment #17
andypostFiled follow-up #3346756: UserInterface::getPassword() can return NULL
Comment #18
paulocsCode looks good and it fixes the errors.
Only one thing: we use snake_case for variables declared in the functions.
$userWithoutPassword = $this->createMock('Drupal\user\Entity\User');I'm attaching a patch and the inter-diff of this change only. So I think I can move to RTBC after tests pass.
Comment #19
smustgrave commentedThink this one is good to go now.
Comment #20
quietone commentedI don't see that the two points in #10 have been addressed. Setting back to NW.
@@ -278,4 +282,30 @@ public function testAddCheckToUrlForTrustedRedirectResponse(): void {
+ * Tests the authenticate method when the user has no stored password.
Let's be clear that this is testing when the user password is NULL.
I think testing when the password is NULL is a valid test and we don't need this extra paragraph. And especially it should not be referring to a d.o issue. I am usually all for comments but I don't think this is adding useful information. What would be useful is to add how the password can be NULL as comments in the fix.
Comment #21
andypostNeeds re-roll as comited #3346756: UserInterface::getPassword() can return NULL
Comment #22
andypostre-roll
Comment #23
smustgrave commentedThink I jumped the gun too early before, that's my mistake. Moving to NW per #20
Comment #24
andypostyes, this extra paragraph is useless, there's git for history
Comment #25
smustgrave commentedThink this is good now.
Comment #26
quietone commentedIn #20 I suggested adding a comment explaining how the password can be NULL for an existing user. I still that is worth doing.
Comment #27
larowlanI don't see an answer to why this shouldn't be done in the password service asked in #10.1
Can we elaborate on why we didn't go that way?
Comment #28
rishabh vishwakarma commentedAdded comment as suggested in #26
Comment #29
larowlanStill needs an answer on 10.1
Comment #30
rishabh vishwakarma commentedFixed CCF from #28
Comment #31
quietone commentedAlso, needs work for #20/#26.. The changes in the patch in #3 didn't add the comment in the fix.
Comment #32
andypostBasically any user created without password will have it as NULL, no idea where to document it
Comment #33
_pratik_patch #30 was not applying, Attached rerolled patch.
Unable to find comments line from #20, #26
thanks
Comment #34
smustgrave commentedPer #31
Comment #35
ilya.no commented@larowlan I agree with your suggestion and here is the patch for your comment #10. I've found, that PasswordHashingTest is now LegacyPasswordHashingTest due to recent changes in password processing logic. So, I've made a change to the new service and to the old one, because as long as this code is presented it needs to work properly although it's deprecated. Please, correct me, if I'm wrong.
Comment #36
andypostThanks @ilya.no I missed this point but I checked and there's only string, empty string and
NULLvalues possibleMoreover in context of #3050720: [Meta] Implement strict typing in existing code it can use follow-up to add deprecation if not a
string|nullis passedI bet we can simplify it with
if ($hash === NULL || $hash === '')because it must be a
stringornullhereComment #37
andypostRe #10.1
I wanted to prevent calling service ("carbonless-optimization") as both places already checking previous argument for NULL to prevent useless function call.
I still sure we need to use the same pattern for saved hash as there's many reasons it could appear a NULL from database
Comment #38
sorlov commentedAdded changes mentioned in #36
Comment #39
andypostRe #38 is wrong pat h as changes only in interdiff
Comment #40
sorlov commentedOh, sorry, here is correct patch
Comment #41
sorlov commentedComment #42
andypostthank you
Comment #43
andypostComment #44
smustgrave commentedSeems part of #10 has been addressed. Think the part that @larowlan was looking for was a positive assertion somewhere.
Comment #46
ilya.no commented@smustgrave I'm not sure, that I've got your point. In comment #10 it's stated, that we need to add test case, which looks like
In my patch I've added following code
which looks the same, as suggestion from comment #10.
Could you correct me, if I'm wrong? Or maybe I didn't understand you. Thanks!
Comment #47
smustgrave commentedYes but it was request for a positive assertion. AssertTrue somewhere
Comment #48
sorlov commentedLogic of latest patch is pretty different from what was addressed in #10
So I don't see how we could have positive assertion for latest patch logic
Comment #49
jessey commentedComment #50
smustgrave commented11.x is the current dev branch.
Also #49 seems to be removing some of the original patch from 40.
Comment #51
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #52
sorlov commentedComment #53
sorlov commentedPatch from #40 can be applied to 11.x as well
Comment #54
smustgrave commentedGoing to see what the committers think if we need to do some trickery for a positive assertion. Going to mark it so it doesn't get lost in the shuffle of things.
Comment #55
catchWhich patch is RTBC? 40 or 49? If it's 40, that should be re-uploaded so it's the newest patch on the issue. I think just a negative assertion is fine, we're testing there's no PHP warning.
Comment #56
ilya.no commented@catch RTBC is for patch #40.
Patch from comment #49 is for 10.0.x branch without any explanation and I think, that we can ignore it.
Comment #57
smustgrave commented@ilya.no hid patch 49.
So #40 should be at the top.
Comment #58
xjmThanks for working on this! A few small things:
I think the same inline comment needs to be added above this hunk as well, unless we want to abstract it into a protected
checkEmptyPassword()method somewhere.Very minor nitpick: This is not a complete sentence, and complete sentences are required by our documentation guidelines:
So, I think this should be:
Shouldn't we also cover empty string in the same method?
Same comments as above.
Comment #59
ChrisPerko commentedI'm working on this issue and @xjm and @allisonherodevs are mentoring me on it.
Comment #60
ChrisPerko commentedAddressed #58 and added return type hints to the new methods.
Comment #62
xjmSo I just spent 15 minutes trying to figure out what the heck was going on with #60. The patches are readable in dreditor or if opened in their own tab. However, when I try to curl them -- or even "File... Save as..." I get an empty file.
I think this is a previously-unknown-to-me security "feature" of Drupal.org in that it won't serve files with the word "password" in the name, which also means DrupalCI gets an empty patch. It's imperfect though because I can open them in the browser in Dreditor or their own tab. Weird, yo.Edit: Hypothesis false per @drumm. Instead, the files might be encoded as UTF-16 and #2922638: No charset on response for patch & text files could then mean curl (and therefore DrupalCI) don't know how to interpret it.What I had to do was create a new file locally, copy the text from the patch opened in a tab, and paste it into my editor. I thought of naming them "hash-nid-etc.patch" but thought that could be, er, misinterpreted.) These are just versions of @ChrisPerko's patch and test-only patch that Drupal.org will deign to serve, so I'm still eligible to review/commit this when appropriate. Fun times!
Anyway, adding credit for Allison as well as the correct mentoring attribution for myself.
Comment #64
xjmComment #65
xjmLooks like stuff is mis-indented which at least is easier to fix. 😂
Comment #66
ChrisPerko commentedI fixed the spacing, I'm crossing my fingers that I created the interdiff and patches correctly.
Comment #67
xjmAll three files seem to contain the interdiff. :)
Comment #68
ChrisPerko commentedHopefully this is right this time. If not, someone else can fix it, or I will get to it Monday morning. Thanks for your help @xjm
Comment #69
xjmThose look like the correct patches, but they still have whatever weird encoding problem unfortunately.
Comment #70
xjmRepeating what I did in #62 except with the original filenames. The interdiff is attached to #68 and works fine since DrupalCI does not need to apply it.
Comment #72
xjmThere we go. :) Correct test failures.
Comment #73
andypostThank you, back to RTBC
Comment #75
larowlanLooking much simpler, nice work.
I think we're missing a documentation update here.
Currently
\Drupal\Core\Password\PasswordInterface::checkdocuments thathashis a string, but we're now effectively saying we also support NULL. I think we should update the @parameter type-hint fromstringtostring|null.Comment #76
asad_ahmed commentedI have updated the @parameter type-hint from string to string|null in core/lib/Drupal/Core/Password/PasswordInterface.php as per #75. Please review. Thanks
Comment #77
smustgrave commentedCC failure
Saving credit for ChrisPerko for keeping this issue moving forward.
Comment #78
andypostFixed CS
this change is out of scope a bit but I included it as part of context
Comment #79
smustgrave commentedChange Looks good.
Comment #80
larowlanThis test didn't fail - and that's because of #3393072: Ensure Unit tests in phpass run and remove unneeded LegacyPasswordHashingTest::testInvalidArguments, so postponing on that
And that's because its in a Tests folder when it should be in a folder named Unit
There are two tests in that folder, and neither of them are running in HEAD
Comment #81
larowlanBlocker is in
Comment #82
andypostreroll after https://git.drupalcode.org/project/drupal/-/commit/9d4f46c05be69bf125f3a...
Comment #83
smustgrave commentedReroll seems good and tests all green!
Comment #87
larowlanCommitted to 11.x and backported to 10.2.x and 10.1.x
Thanks all.
The final patch was nice and small which is always a good sign
Comment #88
andypostThank you! it was the last bit of PHP 8.0 compatibility! Now looking for 8.3)
Comment #89
andypostsorry, the 8.1 compatibility)
Comment #91
aurelianzaha commented@larowlan
It would be great to backport the MR also to 10.3
if there is a way I can help on that, let me know