Skip to content

Commit f444639

Browse files
committed
Security: Switch to using bcrypt for hashing user passwords and BLAKE2b for hashing application passwords and security keys.
Passwords and security keys that were saved in prior versions of WordPress will continue to work. Each user's password will be opportunistically rehashed and resaved when they next subsequently log in using a valid password. The following new functions have been introduced: * `wp_password_needs_rehash()` * `wp_fast_hash()` * `wp_verify_fast_hash()` The following new filters have been introduced: * `password_needs_rehash` * `wp_hash_password_algorithm` * `wp_hash_password_options` Props ayeshrajans, bgermann, dd32, deadduck169, desrosj, haozi, harrym, iandunn, jammycakes, joehoyle, johnbillion, mbijon, mojorob, mslavco, my1xt, nacin, otto42, paragoninitiativeenterprises, paulkevan, rmccue, ryanhellyer, scribu, swalkinshaw, synchro, th23, timothyblynjacobs, tomdxw, westi, xknown. Additional thanks go to the Roots team, Soatok, Calvin Alkan, and Raphael Ahrens. Fixes #21022, #44628 git-svn-id: https://develop.svn.wordpress.org/trunk@59828 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 3b5b6ed commit f444639

File tree

13 files changed

+1269
-137
lines changed

13 files changed

+1269
-137
lines changed

src/wp-admin/includes/upgrade.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,7 @@ function upgrade_101() {
980980
*
981981
* @ignore
982982
* @since 1.2.0
983+
* @since 6.8.0 User passwords are no longer hashed with md5.
983984
*
984985
* @global wpdb $wpdb WordPress database abstraction object.
985986
*/
@@ -995,13 +996,6 @@ function upgrade_110() {
995996
}
996997
}
997998

998-
$users = $wpdb->get_results( "SELECT ID, user_pass from $wpdb->users" );
999-
foreach ( $users as $row ) {
1000-
if ( ! preg_match( '/^[A-Fa-f0-9]{32}$/', $row->user_pass ) ) {
1001-
$wpdb->update( $wpdb->users, array( 'user_pass' => md5( $row->user_pass ) ), array( 'ID' => $row->ID ) );
1002-
}
1003-
}
1004-
1005999
// Get the GMT offset, we'll use that later on.
10061000
$all_options = get_alloptions_110();
10071001

src/wp-includes/class-wp-application-passwords.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public static function is_in_use() {
6060
*
6161
* @since 5.6.0
6262
* @since 5.7.0 Returns WP_Error if application name already exists.
63+
* @since 6.8.0 The hashed password value now uses wp_fast_hash() instead of phpass.
6364
*
6465
* @param int $user_id User ID.
6566
* @param array $args {
@@ -95,7 +96,7 @@ public static function create_new_application_password( $user_id, $args = array(
9596
}
9697

9798
$new_password = wp_generate_password( static::PW_LENGTH, false );
98-
$hashed_password = wp_hash_password( $new_password );
99+
$hashed_password = self::hash_password( $new_password );
99100

100101
$new_item = array(
101102
'uuid' => wp_generate_uuid4(),
@@ -124,6 +125,7 @@ public static function create_new_application_password( $user_id, $args = array(
124125
* Fires when an application password is created.
125126
*
126127
* @since 5.6.0
128+
* @since 6.8.0 The hashed password value now uses wp_fast_hash() instead of phpass.
127129
*
128130
* @param int $user_id The user ID.
129131
* @param array $new_item {
@@ -249,6 +251,7 @@ public static function application_name_exists_for_user( $user_id, $name ) {
249251
* Updates an application password.
250252
*
251253
* @since 5.6.0
254+
* @since 6.8.0 The actual password should now be hashed using wp_fast_hash().
252255
*
253256
* @param int $user_id User ID.
254257
* @param string $uuid The password's UUID.
@@ -296,6 +299,8 @@ public static function update_application_password( $user_id, $uuid, $update = a
296299
* Fires when an application password is updated.
297300
*
298301
* @since 5.6.0
302+
* @since 6.8.0 The password is now hashed using wp_fast_hash() instead of phpass.
303+
* Existing passwords may still be hashed using phpass.
299304
*
300305
* @param int $user_id The user ID.
301306
* @param array $item {
@@ -467,4 +472,36 @@ public static function chunk_password(
467472

468473
return trim( chunk_split( $raw_password, 4, ' ' ) );
469474
}
475+
476+
/**
477+
* Hashes a plaintext application password.
478+
*
479+
* @since 6.8.0
480+
*
481+
* @param string $password Plaintext password.
482+
* @return string Hashed password.
483+
*/
484+
public static function hash_password(
485+
#[\SensitiveParameter]
486+
string $password
487+
): string {
488+
return wp_fast_hash( $password );
489+
}
490+
491+
/**
492+
* Checks a plaintext application password against a hashed password.
493+
*
494+
* @since 6.8.0
495+
*
496+
* @param string $password Plaintext password.
497+
* @param string $hash Hash of the password to check against.
498+
* @return bool Whether the password matches the hashed password.
499+
*/
500+
public static function check_password(
501+
#[\SensitiveParameter]
502+
string $password,
503+
string $hash
504+
): bool {
505+
return wp_verify_fast_hash( $password, $hash );
506+
}
470507
}

src/wp-includes/class-wp-recovery-mode-key-service.php

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -37,29 +37,18 @@ public function generate_recovery_mode_token() {
3737
* Creates a recovery mode key.
3838
*
3939
* @since 5.2.0
40-
*
41-
* @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
40+
* @since 6.8.0 The stored key is now hashed using wp_fast_hash() instead of phpass.
4241
*
4342
* @param string $token A token generated by {@see generate_recovery_mode_token()}.
4443
* @return string Recovery mode key.
4544
*/
4645
public function generate_and_store_recovery_mode_key( $token ) {
47-
48-
global $wp_hasher;
49-
5046
$key = wp_generate_password( 22, false );
5147

52-
if ( empty( $wp_hasher ) ) {
53-
require_once ABSPATH . WPINC . '/class-phpass.php';
54-
$wp_hasher = new PasswordHash( 8, true );
55-
}
56-
57-
$hashed = $wp_hasher->HashPassword( $key );
58-
5948
$records = $this->get_keys();
6049

6150
$records[ $token ] = array(
62-
'hashed_key' => $hashed,
51+
'hashed_key' => wp_fast_hash( $key ),
6352
'created_at' => time(),
6453
);
6554

@@ -85,16 +74,12 @@ public function generate_and_store_recovery_mode_key( $token ) {
8574
*
8675
* @since 5.2.0
8776
*
88-
* @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
89-
*
9077
* @param string $token The token used when generating the given key.
91-
* @param string $key The unhashed key.
78+
* @param string $key The plain text key.
9279
* @param int $ttl Time in seconds for the key to be valid for.
9380
* @return true|WP_Error True on success, error object on failure.
9481
*/
9582
public function validate_recovery_mode_key( $token, $key, $ttl ) {
96-
global $wp_hasher;
97-
9883
$records = $this->get_keys();
9984

10085
if ( ! isset( $records[ $token ] ) ) {
@@ -109,12 +94,7 @@ public function validate_recovery_mode_key( $token, $key, $ttl ) {
10994
return new WP_Error( 'invalid_recovery_key_format', __( 'Invalid recovery key format.' ) );
11095
}
11196

112-
if ( empty( $wp_hasher ) ) {
113-
require_once ABSPATH . WPINC . '/class-phpass.php';
114-
$wp_hasher = new PasswordHash( 8, true );
115-
}
116-
117-
if ( ! $wp_hasher->CheckPassword( $key, $record['hashed_key'] ) ) {
97+
if ( ! wp_verify_fast_hash( $key, $record['hashed_key'] ) ) {
11898
return new WP_Error( 'hash_mismatch', __( 'Invalid recovery key.' ) );
11999
}
120100

@@ -169,9 +149,20 @@ private function remove_key( $token ) {
169149
* Gets the recovery key records.
170150
*
171151
* @since 5.2.0
152+
* @since 6.8.0 Each key is now hashed using wp_fast_hash() instead of phpass.
153+
* Existing keys may still be hashed using phpass.
154+
*
155+
* @return array {
156+
* Associative array of token => data pairs, where the data is an associative
157+
* array of information about the key.
172158
*
173-
* @return array Associative array of $token => $data pairs, where $data has keys 'hashed_key'
174-
* and 'created_at'.
159+
* @type array ...$0 {
160+
* Information about the key.
161+
*
162+
* @type string $hashed_key The hashed value of the key.
163+
* @type int $created_at The timestamp when the key was created.
164+
* }
165+
* }
175166
*/
176167
private function get_keys() {
177168
return (array) get_option( $this->option_name, array() );
@@ -181,9 +172,19 @@ private function get_keys() {
181172
* Updates the recovery key records.
182173
*
183174
* @since 5.2.0
175+
* @since 6.8.0 Each key should now be hashed using wp_fast_hash() instead of phpass.
176+
*
177+
* @param array $keys {
178+
* Associative array of token => data pairs, where the data is an associative
179+
* array of information about the key.
180+
*
181+
* @type array ...$0 {
182+
* Information about the key.
184183
*
185-
* @param array $keys Associative array of $token => $data pairs, where $data has keys 'hashed_key'
186-
* and 'created_at'.
184+
* @type string $hashed_key The hashed value of the key.
185+
* @type int $created_at The timestamp when the key was created.
186+
* }
187+
* }
187188
* @return bool True on success, false on failure.
188189
*/
189190
private function update_keys( array $keys ) {

src/wp-includes/class-wp-user-request.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ final class WP_User_Request {
9292
* Key used to confirm this request.
9393
*
9494
* @since 4.9.6
95+
* @since 6.8.0 The key is now hashed using wp_fast_hash() instead of phpass.
96+
*
9597
* @var string
9698
*/
9799
public $confirm_key = '';

src/wp-includes/class-wp-user.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* Core class used to implement the WP_User object.
1212
*
1313
* @since 2.0.0
14+
* @since 6.8.0 The `user_pass` property is now hashed using bcrypt instead of phpass.
1415
*
1516
* @property string $nickname
1617
* @property string $description

src/wp-includes/functions.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9114,3 +9114,62 @@ function wp_is_heic_image_mime_type( $mime_type ) {
91149114

91159115
return in_array( $mime_type, $heic_mime_types, true );
91169116
}
9117+
9118+
/**
9119+
* Returns a cryptographically secure hash of a message using a fast generic hash function.
9120+
*
9121+
* Use the wp_verify_fast_hash() function to verify the hash.
9122+
*
9123+
* This function does not salt the value prior to being hashed, therefore input to this function must originate from
9124+
* a random generator with sufficiently high entropy, preferably greater than 128 bits. This function is used internally
9125+
* in WordPress to hash security keys and application passwords which are generated with high entropy.
9126+
*
9127+
* Important:
9128+
*
9129+
* - This function must not be used for hashing user-generated passwords. Use wp_hash_password() for that.
9130+
* - This function must not be used for hashing other low-entropy input. Use wp_hash() for that.
9131+
*
9132+
* The BLAKE2b algorithm is used by Sodium to hash the message.
9133+
*
9134+
* @since 6.8.0
9135+
*
9136+
* @throws TypeError Thrown by Sodium if the message is not a string.
9137+
*
9138+
* @param string $message The message to hash.
9139+
* @return string The hash of the message.
9140+
*/
9141+
function wp_fast_hash(
9142+
#[\SensitiveParameter]
9143+
string $message
9144+
): string {
9145+
return '$generic$' . sodium_bin2hex( sodium_crypto_generichash( $message ) );
9146+
}
9147+
9148+
/**
9149+
* Checks whether a plaintext message matches the hashed value. Used to verify values hashed via wp_fast_hash().
9150+
*
9151+
* The function uses Sodium to hash the message and compare it to the hashed value. If the hash is not a generic hash,
9152+
* the hash is treated as a phpass portable hash in order to provide backward compatibility for application passwords
9153+
* which were hashed using phpass prior to WordPress 6.8.0.
9154+
*
9155+
* @since 6.8.0
9156+
*
9157+
* @throws TypeError Thrown by Sodium if the message is not a string.
9158+
*
9159+
* @param string $message The plaintext message.
9160+
* @param string $hash Hash of the message to check against.
9161+
* @return bool Whether the message matches the hashed message.
9162+
*/
9163+
function wp_verify_fast_hash(
9164+
#[\SensitiveParameter]
9165+
string $message,
9166+
string $hash
9167+
): bool {
9168+
if ( ! str_starts_with( $hash, '$generic$' ) ) {
9169+
// Back-compat for old phpass hashes.
9170+
require_once ABSPATH . WPINC . '/class-phpass.php';
9171+
return ( new PasswordHash( 8, true ) )->CheckPassword( $message, $hash );
9172+
}
9173+
9174+
return hash_equals( $hash, wp_fast_hash( $message ) );
9175+
}

0 commit comments

Comments
 (0)