Skip to content

Commit ac1a9c1

Browse files
committed
Merge branch 'PHP-8.4' into PHP-8.5
* PHP-8.4: ext/standard: Validate the bcrypt cost before reading it
2 parents 33d7f34 + 2f7951d commit ac1a9c1

4 files changed

Lines changed: 73 additions & 11 deletions

File tree

‎NEWS‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ PHP NEWS
1919
. Fix too wide type inference for ASSIGN_DIM_OP. (ndossche)
2020
. Fix OSS-Fuzz #568005340 (FETCH_DIM_FUNC_ARG partial conversion). (ndossche)
2121

22+
- Standard:
23+
. Fixed password_get_info() and password_needs_rehash() accepting malformed
24+
bcrypt costs. (Ilia Alshanetsky)
25+
2226
- Zip:
2327
. Fixed use-after-free when re-entering ZipArchive during destruction or
2428
a close warning, and rejected opening streams while closing. (jvoisin)

‎ext/standard/password.c‎

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,37 +113,47 @@ static zend_string* php_password_get_salt(zval *unused_, size_t required_salt_le
113113

114114
/* bcrypt implementation */
115115

116-
static bool php_password_bcrypt_valid(const zend_string *hash) {
116+
static bool php_password_bcrypt_get_cost(const zend_string *hash, zend_long *cost) {
117117
const char *h = ZSTR_VAL(hash);
118-
return (ZSTR_LEN(hash) == 60) &&
119-
(h[0] == '$') && (h[1] == '2') && (h[2] == 'y');
118+
119+
if ((ZSTR_LEN(hash) != 60) ||
120+
(h[0] != '$') || (h[1] != '2') || (h[2] != 'y') || (h[3] != '$') ||
121+
!ZEND_IS_DIGIT(h[4]) || !ZEND_IS_DIGIT(h[5]) || (h[6] != '$')) {
122+
return false;
123+
}
124+
125+
*cost = (h[4] - '0') * 10 + (h[5] - '0');
126+
127+
return *cost >= 4 && *cost <= 31;
128+
}
129+
130+
static bool php_password_bcrypt_valid(const zend_string *hash) {
131+
zend_long cost;
132+
133+
return php_password_bcrypt_get_cost(hash, &cost);
120134
}
121135

122136
static int php_password_bcrypt_get_info(zval *return_value, const zend_string *hash) {
123-
zend_long cost = PHP_PASSWORD_BCRYPT_COST;
137+
zend_long cost;
124138

125-
if (!php_password_bcrypt_valid(hash)) {
139+
if (!php_password_bcrypt_get_cost(hash, &cost)) {
126140
/* Should never get called this way. */
127141
return FAILURE;
128142
}
129-
130-
sscanf(ZSTR_VAL(hash), "$2y$" ZEND_LONG_FMT "$", &cost);
131143
add_assoc_long(return_value, "cost", cost);
132144

133145
return SUCCESS;
134146
}
135147

136148
static bool php_password_bcrypt_needs_rehash(const zend_string *hash, zend_array *options) {
137149
zval *znew_cost;
138-
zend_long old_cost = PHP_PASSWORD_BCRYPT_COST;
150+
zend_long old_cost;
139151
zend_long new_cost = PHP_PASSWORD_BCRYPT_COST;
140152

141-
if (!php_password_bcrypt_valid(hash)) {
153+
if (!php_password_bcrypt_get_cost(hash, &old_cost)) {
142154
/* Should never get called this way. */
143155
return 1;
144156
}
145-
146-
sscanf(ZSTR_VAL(hash), "$2y$" ZEND_LONG_FMT "$", &old_cost);
147157
if (options && (znew_cost = zend_hash_str_find(options, "cost", sizeof("cost")-1)) != NULL) {
148158
new_cost = zval_get_long(znew_cost);
149159
}

‎ext/standard/tests/password/password_get_info.phpt‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@ var_dump(password_get_info('$2y$11$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vj
1212
// Test Non-Bcrypt
1313
var_dump(password_get_info('$1$rasmusle$rISCgZzpwk3UhDidwXvin0'));
1414

15+
// Valid cost boundaries
16+
$suffix = 'MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y';
17+
foreach ([4, 31] as $cost) {
18+
$info = password_get_info('$2y$' . sprintf('%02d', $cost) . '$' . $suffix);
19+
printf("valid %d: %s, cost %d\n", $cost, $info['algoName'], $info['options']['cost']);
20+
}
21+
22+
// Invalid cost grammar and range
23+
$invalidHashes = [
24+
'non-digit' => '$2y$a0$' . $suffix,
25+
'malformed separator' => '$2y$10x' . $suffix,
26+
'below range' => '$2y$03$' . $suffix,
27+
'above range' => '$2y$32$' . $suffix,
28+
'missing separator' => '$2y$' . str_repeat('9', 56),
29+
];
30+
foreach ($invalidHashes as $description => $hash) {
31+
$info = password_get_info($hash);
32+
printf("%s: %s, options %d\n", $description, $info['algoName'], count($info['options']));
33+
}
34+
1535
echo "OK!";
1636
?>
1737
--EXPECT--
@@ -55,4 +75,11 @@ array(3) {
5575
array(0) {
5676
}
5777
}
78+
valid 4: bcrypt, cost 4
79+
valid 31: bcrypt, cost 31
80+
non-digit: unknown, options 0
81+
malformed separator: unknown, options 0
82+
below range: unknown, options 0
83+
above range: unknown, options 0
84+
missing separator: unknown, options 0
5885
OK!

‎ext/standard/tests/password/password_needs_rehash.phpt‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,21 @@ var_dump(password_needs_rehash('$2y$'.$cost.'$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.o
3333
// Should Issue Needs Rehash, Since Foo is cast to 0...
3434
var_dump(password_needs_rehash('$2y$10$MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y', PASSWORD_BCRYPT, array('cost' => 'foo')));
3535

36+
// Valid cost boundaries
37+
$suffix = 'MTIzNDU2Nzg5MDEyMzQ1Nej0NmcAWSLR.oP7XOR9HD/vjUuOj100y';
38+
$costCases = [
39+
'valid lower boundary' => ['$2y$04$' . $suffix, 4],
40+
'valid upper boundary' => ['$2y$31$' . $suffix, 31],
41+
'non-digit' => ['$2y$a0$' . $suffix, 12],
42+
'malformed separator' => ['$2y$10x' . $suffix, 10],
43+
'below range' => ['$2y$03$' . $suffix, 3],
44+
'above range' => ['$2y$32$' . $suffix, 32],
45+
];
46+
foreach ($costCases as $description => $case) {
47+
echo $description, ': ';
48+
var_dump(password_needs_rehash($case[0], PASSWORD_BCRYPT, ['cost' => $case[1]]));
49+
}
50+
3651
// CRYPT_MD5
3752
var_dump(password_needs_rehash(crypt('Example', '$1$'), PASSWORD_DEFAULT));
3853

@@ -54,6 +69,12 @@ bool(true)
5469
bool(true)
5570
bool(false)
5671
bool(true)
72+
valid lower boundary: bool(false)
73+
valid upper boundary: bool(false)
74+
non-digit: bool(true)
75+
malformed separator: bool(true)
76+
below range: bool(true)
77+
above range: bool(true)
5778
bool(true)
5879
bool(true)
5980
OK!

0 commit comments

Comments
 (0)