Skip to content

Commit 9c0ca75

Browse files
committed
bug #40920 [PasswordHasher] accept hashing passwords with nul bytes or longer than 72 bytes when using bcrypt (nicolas-grekas)
This PR was merged into the 5.3-dev branch. Discussion ---------- [PasswordHasher] accept hashing passwords with nul bytes or longer than 72 bytes when using bcrypt | Q | A | ------------- | --- | Branch? | 5.x | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - This limitation of bcrypt creates a risk for migrations. But we can remove it, so here we are. Commits ------- a5d3b89472 [PasswordHasher] accept hashing passwords with nul bytes or longer than 72 bytes when using bcrypt
2 parents c6de8ca + 27c2805 commit 9c0ca75

File tree

4 files changed

+52
-12
lines changed

4 files changed

+52
-12
lines changed

Hasher/NativePasswordHasher.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ public function __construct(int $opsLimit = null, int $memLimit = null, int $cos
5353
$algorithms = [1 => \PASSWORD_BCRYPT, '2y' => \PASSWORD_BCRYPT];
5454

5555
if (\defined('PASSWORD_ARGON2I')) {
56-
$algorithms[2] = $algorithms['argon2i'] = (string) \PASSWORD_ARGON2I;
56+
$algorithms[2] = $algorithms['argon2i'] = \PASSWORD_ARGON2I;
5757
}
5858

5959
if (\defined('PASSWORD_ARGON2ID')) {
60-
$algorithms[3] = $algorithms['argon2id'] = (string) \PASSWORD_ARGON2ID;
60+
$algorithms[3] = $algorithms['argon2id'] = \PASSWORD_ARGON2ID;
6161
}
6262

6363
$this->algorithm = $algorithms[$algorithm] ?? $algorithm;
@@ -73,10 +73,14 @@ public function __construct(int $opsLimit = null, int $memLimit = null, int $cos
7373

7474
public function hash(string $plainPassword): string
7575
{
76-
if ($this->isPasswordTooLong($plainPassword) || ((string) \PASSWORD_BCRYPT === $this->algorithm && 72 < \strlen($plainPassword))) {
76+
if ($this->isPasswordTooLong($plainPassword)) {
7777
throw new InvalidPasswordException();
7878
}
7979

80+
if (\PASSWORD_BCRYPT === $this->algorithm && (72 < \strlen($plainPassword) || false !== strpos($plainPassword, "\0"))) {
81+
$plainPassword = base64_encode(hash('sha512', $plainPassword, true));
82+
}
83+
8084
return password_hash($plainPassword, $this->algorithm, $this->options);
8185
}
8286

@@ -87,8 +91,12 @@ public function verify(string $hashedPassword, string $plainPassword): bool
8791
}
8892

8993
if (0 !== strpos($hashedPassword, '$argon')) {
90-
// BCrypt encodes only the first 72 chars
91-
return (72 >= \strlen($plainPassword) || 0 !== strpos($hashedPassword, '$2')) && password_verify($plainPassword, $hashedPassword);
94+
// Bcrypt cuts on NUL chars and after 72 bytes
95+
if (0 === strpos($hashedPassword, '$2') && (72 < \strlen($plainPassword) || false !== strpos($plainPassword, "\0"))) {
96+
$plainPassword = base64_encode(hash('sha512', $plainPassword, true));
97+
}
98+
99+
return password_verify($plainPassword, $hashedPassword);
92100
}
93101

94102
if (\extension_loaded('sodium') && version_compare(\SODIUM_LIBRARY_VERSION, '1.0.14', '>=')) {

Hasher/SodiumPasswordHasher.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,12 @@ public function verify(string $hashedPassword, string $plainPassword): bool
8080
}
8181

8282
if (0 !== strpos($hashedPassword, '$argon')) {
83+
if (0 === strpos($hashedPassword, '$2') && (72 < \strlen($plainPassword) || false !== strpos($plainPassword, "\0"))) {
84+
$plainPassword = base64_encode(hash('sha512', $plainPassword, true));
85+
}
86+
8387
// Accept validating non-argon passwords for seamless migrations
84-
return (72 >= \strlen($plainPassword) || 0 !== strpos($hashedPassword, '$2')) && password_verify($plainPassword, $hashedPassword);
88+
return password_verify($plainPassword, $hashedPassword);
8589
}
8690

8791
if (\function_exists('sodium_crypto_pwhash_str_verify')) {

Tests/Hasher/NativePasswordHasherTest.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,22 @@ public function testConfiguredAlgorithmWithLegacyConstValue()
8989
$this->assertStringStartsWith('$2', $result);
9090
}
9191

92-
public function testCheckPasswordLength()
92+
public function testBcryptWithLongPassword()
9393
{
94-
$hasher = new NativePasswordHasher(null, null, 4);
95-
$result = password_hash(str_repeat('a', 72), \PASSWORD_BCRYPT, ['cost' => 4]);
94+
$hasher = new NativePasswordHasher(null, null, 4, \PASSWORD_BCRYPT);
95+
$plainPassword = str_repeat('a', 100);
9696

97-
$this->assertFalse($hasher->verify($result, str_repeat('a', 73), 'salt'));
98-
$this->assertTrue($hasher->verify($result, str_repeat('a', 72), 'salt'));
97+
$this->assertFalse($hasher->verify(password_hash($plainPassword, \PASSWORD_BCRYPT, ['cost' => 4]), $plainPassword, 'salt'));
98+
$this->assertTrue($hasher->verify($hasher->hash($plainPassword), $plainPassword, 'salt'));
99+
}
100+
101+
public function testBcryptWithNulByte()
102+
{
103+
$hasher = new NativePasswordHasher(null, null, 4, \PASSWORD_BCRYPT);
104+
$plainPassword = "a\0b";
105+
106+
$this->assertFalse($hasher->verify(password_hash($plainPassword, \PASSWORD_BCRYPT, ['cost' => 4]), $plainPassword, 'salt'));
107+
$this->assertTrue($hasher->verify($hasher->hash($plainPassword), $plainPassword, 'salt'));
99108
}
100109

101110
public function testNeedsRehash()

Tests/Hasher/SodiumPasswordHasherTest.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\PasswordHasher\Exception\InvalidPasswordException;
16+
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
1617
use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher;
1718

1819
class SodiumPasswordHasherTest extends TestCase
@@ -33,7 +34,7 @@ public function testValidation()
3334
$this->assertFalse($hasher->verify($result, '', null));
3435
}
3536

36-
public function testBCryptValidation()
37+
public function testBcryptValidation()
3738
{
3839
$hasher = new SodiumPasswordHasher();
3940
$this->assertTrue($hasher->verify('$2y$04$M8GDODMoGQLQRpkYCdoJh.lbiZPee3SZI32RcYK49XYTolDGwoRMm', 'abc', null));
@@ -63,6 +64,24 @@ public function testCheckPasswordLength()
6364
$this->assertTrue($hasher->verify($result, str_repeat('a', 4096), null));
6465
}
6566

67+
public function testBcryptWithLongPassword()
68+
{
69+
$hasher = new SodiumPasswordHasher(null, null, 4);
70+
$plainPassword = str_repeat('a', 100);
71+
72+
$this->assertFalse($hasher->verify(password_hash($plainPassword, \PASSWORD_BCRYPT, ['cost' => 4]), $plainPassword, 'salt'));
73+
$this->assertTrue($hasher->verify((new NativePasswordHasher(null, null, 4, \PASSWORD_BCRYPT))->hash($plainPassword), $plainPassword, 'salt'));
74+
}
75+
76+
public function testBcryptWithNulByte()
77+
{
78+
$hasher = new SodiumPasswordHasher(null, null, 4);
79+
$plainPassword = "a\0b";
80+
81+
$this->assertFalse($hasher->verify(password_hash($plainPassword, \PASSWORD_BCRYPT, ['cost' => 4]), $plainPassword, 'salt'));
82+
$this->assertTrue($hasher->verify((new NativePasswordHasher(null, null, 4, \PASSWORD_BCRYPT))->hash($plainPassword), $plainPassword, 'salt'));
83+
}
84+
6685
public function testUserProvidedSaltIsNotUsed()
6786
{
6887
$hasher = new SodiumPasswordHasher();

0 commit comments

Comments
 (0)