Skip to content

[PasswordHasher] Make bcrypt nul byte hash test tolerant to PHP related failures #54858

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 10, 2024

Conversation

alexandre-daubois
Copy link
Member

Q A
Branch? 5.4
Bug fix? yes
New feature? no
Deprecations? no
Issues -
License MIT

Bcrypt throws on PHP < 8.2 when passing a nul byte. The related test should be skipped for these versions as $hasher->verify() return value cannot be asserted.

Related failure: https://github.com/symfony/symfony/actions/runs/8980661047/job/24664698662#step:7:1168

@derrabus
Copy link
Member

derrabus commented May 7, 2024

These tests already contain a PHP version detection logic for this purpose. Should we rather fix that instead of adding another layer?

@alexandre-daubois
Copy link
Member Author

alexandre-daubois commented May 7, 2024

You mean adding if (\PHP_VERSION_ID < 80200) { $this->markAsSkipped(...); } at the top of the test instead of having the annotation ?

@xabbuh
Copy link
Member

xabbuh commented May 7, 2024

if (\PHP_VERSION_ID < 80218 || \PHP_VERSION_ID >= 80300 && \PHP_VERSION_ID < 80305) {
    // password_hash() does not accept passwords containing NUL bytes since PHP 8.2.18 and 8.3.5
    $this->assertFalse($hasher->verify(password_hash($plainPassword, \PASSWORD_BCRYPT, ['cost' => 4]), $plainPassword));
}

This part should probably be updated.

@alexandre-daubois
Copy link
Member Author

alexandre-daubois commented May 7, 2024

But it is actually a bit different. In the part you highlight, we still try to hash+verify, whereas prior to 8.2, password_hash() directly throws. $hasher->verify() is then skipped. Because of this, it doesn't seems relevant to me to trigger the test before PHP 8.2. 🤔

Checking password_hash() actually throws something would be the same as testing the function itself, which is probably already done somewhere in the PHP engine.

However, changing from the annotation to an if statement at the beginning of the test case suits me fine 🙂

@xabbuh
Copy link
Member

xabbuh commented May 7, 2024

$this->assertTrue($hasher->verify($hasher->hash($plainPassword), $plainPassword));

This line should always be executed as our password hasher makes sure to not pass the nul byte to password_hash(). That's why we have the if. I actually do not really understand why the PHP 7.2 and 7.4 suddenly fail as the changes in PHP I referenced in #54587 did not happen on these versions. 🤔

@derrabus
Copy link
Member

derrabus commented May 7, 2024

What puzzles me here: PHP 7.2 is around for a while already, why does this test start to become a problem just now?

@alexandre-daubois
Copy link
Member Author

Ok I understand what I missed, I didn't see that the calls in assertFalse() and assertTrue() were different 🤦 I'll investigate a bit more and reopen something when I have the right fix

@xabbuh
Copy link
Member

xabbuh commented May 7, 2024

What puzzles me here: PHP 7.2 is around for a while already, why does this test start to become a problem just now?

same for me, could it be that the Docker image that we use in the CI uses PHP versions that have patches backported? 🤔

@alexandre-daubois
Copy link
Member Author

Here is the commit responsible of the failure: php/php-src@11f2568

@derrabus
Copy link
Member

derrabus commented May 7, 2024

same for me, could it be that the Docker image that we use in the CI uses PHP versions that have patches backported? 🤔

In that case, shall we drop the version detection entirely and make the test tolerant to the error raised by PHP?

@alexandre-daubois
Copy link
Member Author

It seems that password_hash() doesn't support \0 at all now. Should NativePasswordHasher but updated to forbid the nul char? Currently, it allows it if the password is more than 72 chars long

@xabbuh
Copy link
Member

xabbuh commented May 7, 2024

for a job based on PHP 7.4.33 I would have expected to to have that same instruction appear in https://github.com/php/php-src/blob/PHP-7.4.33/ext/standard/password.c too

@xabbuh
Copy link
Member

xabbuh commented May 7, 2024

Currently, it allows it if the password is more than 72 chars long

Nope, when a plain password contains nul it's always sha512 hashed and base 64 encoded (the same is also true if the password is longer than or equal to 72 characters no matter if it contains nul).

@derrabus
Copy link
Member

derrabus commented May 7, 2024

Maybe @shivammathur can tell us if that patch has been backported to his PHP 7 builds. Sorry for the shameless ping. 😓

@alexandre-daubois
Copy link
Member Author

when a plain password contains nul it's always sha512 hashed and base 64 encoded

Oh yes my bad, nothing to do in the hasher indeed

@derrabus
Copy link
Member

derrabus commented May 7, 2024

Answering my own question: Yes, the patch has been backported as shivammathur/php-src-backports@d22d9eb.

Much love for this repository, @shivammathur. ❤️

In that case, I'm back to my proposal in #54858 (comment)

@xabbuh
Copy link
Member

xabbuh commented May 7, 2024

In that case, shall we drop the version detection entirely and make the test tolerant to the error raised by PHP?

yes, makes sense to me then

@alexandre-daubois alexandre-daubois changed the title [PasswordHasher] Skip test on nul byte for PHP versions that doesn't support it with Bcrypt [PasswordHasher] Make bcrypt nul byte hash test tolerant to PHP related failures May 7, 2024
@alexandre-daubois
Copy link
Member Author

What do you think of this new approach?

Comment on lines 106 to 111
try {
$verified = $hasher->verify($hasher->hash($plainPassword), $plainPassword);

$this->assertTrue($hasher->verify($hasher->hash($plainPassword), $plainPassword));
$this->assertTrue($verified);
} catch (\ValueError $error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the try block minimal and only include the call that you expect to raise the ValueError. Given that ValueError is a very generic exception class, we should also sniff the exception message to make sure we're skipping the test for the right reasons.

Please also add a clarifying comment hinting at the PHP commit that caused this change.

I think, we should also skip the test for PHP >= 8.4 entirely (via annotation). This way, we can find and delete it once we open a branch that doesn't support PHP 8.3 anymore (likely Symfony 8.0).

$this->assertTrue($hasher->verify($hasher->hash($plainPassword), $plainPassword));
$this->assertTrue($verified);
} catch (\ValueError $error) {
$this->markTestSkipped(sprintf('password_hash() does not accept passwords containing NUL bytes on PHP %s. ', \PHP_VERSION));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$this->markTestSkipped(sprintf('password_hash() does not accept passwords containing NUL bytes on PHP %s. ', \PHP_VERSION));
$this->markTestSkipped('password_hash() does not accept passwords containing NUL bytes.');

I would not include the current PHP version here.

@alexandre-daubois alexandre-daubois force-pushed the bcrypt-throws branch 2 times, most recently from 5f131e3 to acb87ac Compare May 7, 2024 10:02
$this->assertFalse($hasher->verify(password_hash($plainPassword, \PASSWORD_BCRYPT, ['cost' => 4]), $plainPassword));
try {
$hash = password_hash($plainPassword, \PASSWORD_BCRYPT, ['cost' => 4]);
} catch (\ValueError $error) {
Copy link
Member

@derrabus derrabus May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ValueError error class does not exist on PHP 7. Please double check what the backport raises instead. Maybe catching Throwable would be okay here, given that we filter out the exception message.

Copy link
Member Author

@alexandre-daubois alexandre-daubois May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated so we check for a Throwable with a specific message. I also checked the case password_hash() returns null, as implemented in the backport.

@alexandre-daubois alexandre-daubois force-pushed the bcrypt-throws branch 4 times, most recently from b748ada to c25c368 Compare May 7, 2024 11:29
@xabbuh
Copy link
Member

xabbuh commented May 10, 2024

Thank you @alexandre-daubois.

@xabbuh xabbuh merged commit 6750f6c into symfony:5.4 May 10, 2024
9 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants