Skip to content

Fix the implementation of BlowFish to solve bugs concerning NUL bytes truncating strings#21675

Open
LamentXU123 wants to merge 4 commits intophp:masterfrom
LamentXU123:vul-2
Open

Fix the implementation of BlowFish to solve bugs concerning NUL bytes truncating strings#21675
LamentXU123 wants to merge 4 commits intophp:masterfrom
LamentXU123:vul-2

Conversation

@LamentXU123
Copy link
Copy Markdown
Contributor

@LamentXU123 LamentXU123 commented Apr 8, 2026

Now, since including \0 in password_hash is not allowed in PASSWORD_BCRYPT. It is reasonable to add checkers that, makes input that includes \0 in password_verify directly fails.

This PR align password_verify() with password_hash() for bcrypt by rejecting passwords containing NUL bytes before verification.

Because bcrypt treats passwords as NUL-terminated C strings, this avoids ambiguous verification behavior for inputs such as "foo\0bar". A regression test is included.

Fix #21673

To be short: password encrypted by BYCRYPT should never includes \0. So directly rejecting it is always correct. It also avoids ambiguous verification where something like this happens:

<?php
$hash = password_hash("secret", PASSWORD_BCRYPT);
var_dump(password_verify("secret" . chr(0) . "suffix", $hash)); //True, WTF?
?>

zend_long cost = PHP_PASSWORD_BCRYPT_COST;

if (memchr(ZSTR_VAL(password), '\0', ZSTR_LEN(password))) {
if (zend_str_has_nul_byte(password)) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

nit: I also refactor this by the way.

@alecpl
Copy link
Copy Markdown

alecpl commented Apr 9, 2026

Shouldn't that be rather done in php_password_bcrypt_verify()?

@LamentXU123
Copy link
Copy Markdown
Contributor Author

Shouldn't that be rather done in php_password_bcrypt_verify()?

It is before, see the initial fix in 6f14bee

Actually I think that'd be better. Anyway, lets wait for code owners.

@Girgias
Copy link
Copy Markdown
Member

Girgias commented Apr 9, 2026

Can we not fix the blowfish implementation? It seems like it requires providing the length of the string all the way down to BF_set_key. Or does BlowFish specifically not support null bytes?

@LamentXU123
Copy link
Copy Markdown
Contributor Author

LamentXU123 commented Apr 9, 2026

Does BlowFish specifically not support null bytes?

Blowfish do support null bytes, but in php implementation it specifically doesn't. Any password with NUL will fail when hashing by using password_hash in bcrypt mode.

bugs with NUL bytes is inevitable since PHP is based on C string. But in this case, I think directly rejecting all inputs containing NUL bytes will be safer and always correct.

@Girgias
Copy link
Copy Markdown
Member

Girgias commented Apr 10, 2026

Does BlowFish specifically not support null bytes?

Blowfish do support null bytes, but in php implementation it specifically doesn't. Any password with NUL will fail when hashing by using password_hash in bcrypt mode.

bugs with NUL bytes is inevitable since PHP is based on C string. But in this case, I think directly rejecting all inputs containing NUL bytes will be safer and always correct.

Sure, but we "own" the implementation of BF, so why not actually fix the implementation? I'm happy to reject them as a bug fix that is backported, but in the long run we should fix the implementation.

@LamentXU123
Copy link
Copy Markdown
Contributor Author

Does BlowFish specifically not support null bytes?

Blowfish do support null bytes, but in php implementation it specifically doesn't. Any password with NUL will fail when hashing by using password_hash in bcrypt mode.
bugs with NUL bytes is inevitable since PHP is based on C string. But in this case, I think directly rejecting all inputs containing NUL bytes will be safer and always correct.

Sure, but we "own" the implementation of BF, so why not actually fix the implementation? I'm happy to reject them as a bug fix that is backported, but in the long run we should fix the implementation.

In the long run we should. But for now, password_hash does not accept password containing NUL, so fixing the implementation would create a BC break on this which requires a RFC. I would be happy to write such RFC in the future but I think this bug could be fix before we get it passed.

@Girgias
Copy link
Copy Markdown
Member

Girgias commented Apr 10, 2026

I don't think fixing the implementation to be correct is a BC break.
Your bug fix is a BC break by this logic...

@LamentXU123
Copy link
Copy Markdown
Contributor Author

I don't think fixing the implementation to be correct is a BC break.

That'd be good. I will work on this hours later.

I assume that the best practice here would be passing the correct length throughout the implementation so the strings will not be cut by NUL

@Girgias
Copy link
Copy Markdown
Member

Girgias commented Apr 10, 2026

I don't think fixing the implementation to be correct is a BC break.

That'd be good. I will work on this hours later.

I assume that the best practice here would be passing the correct length throughout the implementation so the strings will not be cut by NUL

Correct :)

@LamentXU123 LamentXU123 changed the title Fix GH-21673: password_verify() failed to verify bcrypt passwords containing null bytes Fix the implementation of BlowFish to solve bugs concerning NUL bytes truncating strings Apr 10, 2026
}
/* }}} */

PHPAPI zend_string *php_crypt(const char *password, const int pass_len, const char *salt, int salt_len, bool quiet)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Might make sense to fix the pass_len param to be size_t

Comment on lines 599 to +603
if (j)
sign |= tmp[1] & 0x80;
if (!*ptr)
ptr = key;
else
ptr++;
key_pos++;
if (key_pos > key_len)
key_pos = 0;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I know this is existing but we usually try to wrap if conditions in {}

Comment on lines +6 to +8
if (crypt("foo\0bar", $setting) === crypt("foo", $setting)) {
die("skip bcrypt backend truncates NUL bytes");
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we ever not rely on our own implementation of crypt? I feel like at one point we decided to always use the custom PHP one... but I may be misremembering.

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.

password_verify() failed to verify bcrypt passwords containing null bytes

4 participants