Security: Switch to using bcrypt for hashing user passwords and BLAKE2b for hashing application passwords and security keys.
Some checks are pending
Cleanup Pull Requests / Clean up pull requests (push) Waiting to run
Coding Standards / PHP coding standards (push) Waiting to run
Coding Standards / JavaScript coding standards (push) Waiting to run
Coding Standards / Slack Notifications (push) Blocked by required conditions
Coding Standards / Failed workflow tasks (push) Blocked by required conditions
End-to-end Tests / Test with SCRIPT_DEBUG disabled (push) Waiting to run
End-to-end Tests / Test with SCRIPT_DEBUG enabled (push) Waiting to run
End-to-end Tests / Slack Notifications (push) Blocked by required conditions
End-to-end Tests / Failed workflow tasks (push) Blocked by required conditions
JavaScript Tests / QUnit Tests (push) Waiting to run
JavaScript Tests / Slack Notifications (push) Blocked by required conditions
JavaScript Tests / Failed workflow tasks (push) Blocked by required conditions
Performance Tests / Determine Matrix (push) Waiting to run
Performance Tests / ${{ matrix.multisite && 'Multisite' || 'Single Site' }} ${{ matrix.memcached && 'Memcached' || 'Default' }} (push) Blocked by required conditions
Performance Tests / Compare (push) Blocked by required conditions
Performance Tests / Slack Notifications (push) Blocked by required conditions
Performance Tests / Failed workflow tasks (push) Blocked by required conditions
PHP Compatibility / Check PHP compatibility (push) Waiting to run
PHP Compatibility / Slack Notifications (push) Blocked by required conditions
PHP Compatibility / Failed workflow tasks (push) Blocked by required conditions
PHPUnit Tests / PHP 7.2 (push) Waiting to run
PHPUnit Tests / PHP 7.3 (push) Waiting to run
PHPUnit Tests / PHP 7.4 (push) Waiting to run
PHPUnit Tests / PHP 8.0 (push) Waiting to run
PHPUnit Tests / PHP 8.1 (push) Waiting to run
PHPUnit Tests / PHP 8.2 (push) Waiting to run
PHPUnit Tests / PHP 8.3 (push) Waiting to run
PHPUnit Tests / PHP 8.4 (push) Waiting to run
PHPUnit Tests / html-api-html5lib-tests (push) Waiting to run
PHPUnit Tests / Slack Notifications (push) Blocked by required conditions
PHPUnit Tests / Failed workflow tasks (push) Blocked by required conditions
Test Build Processes / Core running from build (push) Waiting to run
Test Build Processes / Core running from src (push) Waiting to run
Test Build Processes / Gutenberg running from build (push) Waiting to run
Test Build Processes / Gutenberg running from src (push) Waiting to run
Test Build Processes / Slack Notifications (push) Blocked by required conditions
Test Build Processes / Failed workflow tasks (push) Blocked by required conditions
Upgrade Develop Version Tests / Build (push) Waiting to run
Upgrade Develop Version Tests / Upgrade from 6.5 (push) Blocked by required conditions
Upgrade Develop Version Tests / Upgrade from 6.6 (push) Blocked by required conditions
Upgrade Develop Version Tests / Upgrade from 6.7 (push) Blocked by required conditions
Upgrade Develop Version Tests / Slack Notifications (push) Blocked by required conditions
Upgrade Develop Version Tests / Failed workflow tasks (push) Blocked by required conditions

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
This commit is contained in:
John Blackbourn 2025-02-17 11:22:33 +00:00
parent 3b5b6ed606
commit f444639e08
13 changed files with 1272 additions and 140 deletions

View File

@ -980,6 +980,7 @@ function upgrade_101() {
*
* @ignore
* @since 1.2.0
* @since 6.8.0 User passwords are no longer hashed with md5.
*
* @global wpdb $wpdb WordPress database abstraction object.
*/
@ -995,13 +996,6 @@ function upgrade_110() {
}
}
$users = $wpdb->get_results( "SELECT ID, user_pass from $wpdb->users" );
foreach ( $users as $row ) {
if ( ! preg_match( '/^[A-Fa-f0-9]{32}$/', $row->user_pass ) ) {
$wpdb->update( $wpdb->users, array( 'user_pass' => md5( $row->user_pass ) ), array( 'ID' => $row->ID ) );
}
}
// Get the GMT offset, we'll use that later on.
$all_options = get_alloptions_110();

View File

@ -60,6 +60,7 @@ class WP_Application_Passwords {
*
* @since 5.6.0
* @since 5.7.0 Returns WP_Error if application name already exists.
* @since 6.8.0 The hashed password value now uses wp_fast_hash() instead of phpass.
*
* @param int $user_id User ID.
* @param array $args {
@ -95,7 +96,7 @@ class WP_Application_Passwords {
}
$new_password = wp_generate_password( static::PW_LENGTH, false );
$hashed_password = wp_hash_password( $new_password );
$hashed_password = self::hash_password( $new_password );
$new_item = array(
'uuid' => wp_generate_uuid4(),
@ -124,6 +125,7 @@ class WP_Application_Passwords {
* Fires when an application password is created.
*
* @since 5.6.0
* @since 6.8.0 The hashed password value now uses wp_fast_hash() instead of phpass.
*
* @param int $user_id The user ID.
* @param array $new_item {
@ -249,6 +251,7 @@ class WP_Application_Passwords {
* Updates an application password.
*
* @since 5.6.0
* @since 6.8.0 The actual password should now be hashed using wp_fast_hash().
*
* @param int $user_id User ID.
* @param string $uuid The password's UUID.
@ -296,6 +299,8 @@ class WP_Application_Passwords {
* Fires when an application password is updated.
*
* @since 5.6.0
* @since 6.8.0 The password is now hashed using wp_fast_hash() instead of phpass.
* Existing passwords may still be hashed using phpass.
*
* @param int $user_id The user ID.
* @param array $item {
@ -467,4 +472,36 @@ class WP_Application_Passwords {
return trim( chunk_split( $raw_password, 4, ' ' ) );
}
/**
* Hashes a plaintext application password.
*
* @since 6.8.0
*
* @param string $password Plaintext password.
* @return string Hashed password.
*/
public static function hash_password(
#[\SensitiveParameter]
string $password
): string {
return wp_fast_hash( $password );
}
/**
* Checks a plaintext application password against a hashed password.
*
* @since 6.8.0
*
* @param string $password Plaintext password.
* @param string $hash Hash of the password to check against.
* @return bool Whether the password matches the hashed password.
*/
public static function check_password(
#[\SensitiveParameter]
string $password,
string $hash
): bool {
return wp_verify_fast_hash( $password, $hash );
}
}

View File

@ -37,29 +37,18 @@ final class WP_Recovery_Mode_Key_Service {
* Creates a recovery mode key.
*
* @since 5.2.0
*
* @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
* @since 6.8.0 The stored key is now hashed using wp_fast_hash() instead of phpass.
*
* @param string $token A token generated by {@see generate_recovery_mode_token()}.
* @return string Recovery mode key.
*/
public function generate_and_store_recovery_mode_key( $token ) {
global $wp_hasher;
$key = wp_generate_password( 22, false );
if ( empty( $wp_hasher ) ) {
require_once ABSPATH . WPINC . '/class-phpass.php';
$wp_hasher = new PasswordHash( 8, true );
}
$hashed = $wp_hasher->HashPassword( $key );
$records = $this->get_keys();
$records[ $token ] = array(
'hashed_key' => $hashed,
'hashed_key' => wp_fast_hash( $key ),
'created_at' => time(),
);
@ -85,16 +74,12 @@ final class WP_Recovery_Mode_Key_Service {
*
* @since 5.2.0
*
* @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
*
* @param string $token The token used when generating the given key.
* @param string $key The unhashed key.
* @param string $key The plain text key.
* @param int $ttl Time in seconds for the key to be valid for.
* @return true|WP_Error True on success, error object on failure.
*/
public function validate_recovery_mode_key( $token, $key, $ttl ) {
global $wp_hasher;
$records = $this->get_keys();
if ( ! isset( $records[ $token ] ) ) {
@ -109,12 +94,7 @@ final class WP_Recovery_Mode_Key_Service {
return new WP_Error( 'invalid_recovery_key_format', __( 'Invalid recovery key format.' ) );
}
if ( empty( $wp_hasher ) ) {
require_once ABSPATH . WPINC . '/class-phpass.php';
$wp_hasher = new PasswordHash( 8, true );
}
if ( ! $wp_hasher->CheckPassword( $key, $record['hashed_key'] ) ) {
if ( ! wp_verify_fast_hash( $key, $record['hashed_key'] ) ) {
return new WP_Error( 'hash_mismatch', __( 'Invalid recovery key.' ) );
}
@ -169,9 +149,20 @@ final class WP_Recovery_Mode_Key_Service {
* Gets the recovery key records.
*
* @since 5.2.0
* @since 6.8.0 Each key is now hashed using wp_fast_hash() instead of phpass.
* Existing keys may still be hashed using phpass.
*
* @return array Associative array of $token => $data pairs, where $data has keys 'hashed_key'
* and 'created_at'.
* @return array {
* Associative array of token => data pairs, where the data is an associative
* array of information about the key.
*
* @type array ...$0 {
* Information about the key.
*
* @type string $hashed_key The hashed value of the key.
* @type int $created_at The timestamp when the key was created.
* }
* }
*/
private function get_keys() {
return (array) get_option( $this->option_name, array() );
@ -181,9 +172,19 @@ final class WP_Recovery_Mode_Key_Service {
* Updates the recovery key records.
*
* @since 5.2.0
* @since 6.8.0 Each key should now be hashed using wp_fast_hash() instead of phpass.
*
* @param array $keys Associative array of $token => $data pairs, where $data has keys 'hashed_key'
* and 'created_at'.
* @param array $keys {
* Associative array of token => data pairs, where the data is an associative
* array of information about the key.
*
* @type array ...$0 {
* Information about the key.
*
* @type string $hashed_key The hashed value of the key.
* @type int $created_at The timestamp when the key was created.
* }
* }
* @return bool True on success, false on failure.
*/
private function update_keys( array $keys ) {

View File

@ -92,6 +92,8 @@ final class WP_User_Request {
* Key used to confirm this request.
*
* @since 4.9.6
* @since 6.8.0 The key is now hashed using wp_fast_hash() instead of phpass.
*
* @var string
*/
public $confirm_key = '';

View File

@ -11,6 +11,7 @@
* Core class used to implement the WP_User object.
*
* @since 2.0.0
* @since 6.8.0 The `user_pass` property is now hashed using bcrypt instead of phpass.
*
* @property string $nickname
* @property string $description

View File

@ -9114,3 +9114,62 @@ function wp_is_heic_image_mime_type( $mime_type ) {
return in_array( $mime_type, $heic_mime_types, true );
}
/**
* Returns a cryptographically secure hash of a message using a fast generic hash function.
*
* Use the wp_verify_fast_hash() function to verify the hash.
*
* This function does not salt the value prior to being hashed, therefore input to this function must originate from
* a random generator with sufficiently high entropy, preferably greater than 128 bits. This function is used internally
* in WordPress to hash security keys and application passwords which are generated with high entropy.
*
* Important:
*
* - This function must not be used for hashing user-generated passwords. Use wp_hash_password() for that.
* - This function must not be used for hashing other low-entropy input. Use wp_hash() for that.
*
* The BLAKE2b algorithm is used by Sodium to hash the message.
*
* @since 6.8.0
*
* @throws TypeError Thrown by Sodium if the message is not a string.
*
* @param string $message The message to hash.
* @return string The hash of the message.
*/
function wp_fast_hash(
#[\SensitiveParameter]
string $message
): string {
return '$generic$' . sodium_bin2hex( sodium_crypto_generichash( $message ) );
}
/**
* Checks whether a plaintext message matches the hashed value. Used to verify values hashed via wp_fast_hash().
*
* The function uses Sodium to hash the message and compare it to the hashed value. If the hash is not a generic hash,
* the hash is treated as a phpass portable hash in order to provide backward compatibility for application passwords
* which were hashed using phpass prior to WordPress 6.8.0.
*
* @since 6.8.0
*
* @throws TypeError Thrown by Sodium if the message is not a string.
*
* @param string $message The plaintext message.
* @param string $hash Hash of the message to check against.
* @return bool Whether the message matches the hashed message.
*/
function wp_verify_fast_hash(
#[\SensitiveParameter]
string $message,
string $hash
): bool {
if ( ! str_starts_with( $hash, '$generic$' ) ) {
// Back-compat for old phpass hashes.
require_once ABSPATH . WPINC . '/class-phpass.php';
return ( new PasswordHash( 8, true ) )->CheckPassword( $message, $hash );
}
return hash_equals( $hash, wp_fast_hash( $message ) );
}

View File

@ -693,6 +693,7 @@ if ( ! function_exists( 'wp_validate_auth_cookie' ) ) :
*
* @param string $cookie Optional. If used, will validate contents instead of cookie's.
* @param string $scheme Optional. The cookie scheme to use: 'auth', 'secure_auth', or 'logged_in'.
* Note: This does *not* default to 'auth' like other cookie functions.
* @return int|false User ID if valid cookie, false if invalid.
*/
function wp_validate_auth_cookie( $cookie = '', $scheme = '' ) {
@ -768,7 +769,13 @@ if ( ! function_exists( 'wp_validate_auth_cookie' ) ) :
return false;
}
$pass_frag = substr( $user->user_pass, 8, 4 );
if ( str_starts_with( $user->user_pass, '$P$' ) || str_starts_with( $user->user_pass, '$2y$' ) ) {
// Retain previous behaviour of phpass or vanilla bcrypt hashed passwords.
$pass_frag = substr( $user->user_pass, 8, 4 );
} else {
// Otherwise, use a substring from the end of the hash to avoid dealing with potentially long hash prefixes.
$pass_frag = substr( $user->user_pass, -4 );
}
$key = wp_hash( $username . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme );
@ -869,7 +876,13 @@ if ( ! function_exists( 'wp_generate_auth_cookie' ) ) :
$token = $manager->create( $expiration );
}
$pass_frag = substr( $user->user_pass, 8, 4 );
if ( str_starts_with( $user->user_pass, '$P$' ) || str_starts_with( $user->user_pass, '$2y$' ) ) {
// Retain previous behaviour of phpass or vanilla bcrypt hashed passwords.
$pass_frag = substr( $user->user_pass, 8, 4 );
} else {
// Otherwise, use a substring from the end of the hash to avoid dealing with potentially long hash prefixes.
$pass_frag = substr( $user->user_pass, -4 );
}
$key = wp_hash( $user->user_login . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme );
@ -2625,8 +2638,9 @@ if ( ! function_exists( 'wp_hash_password' ) ) :
* instead use the other package password hashing algorithm.
*
* @since 2.5.0
* @since 6.8.0 The password is now hashed using bcrypt by default instead of phpass.
*
* @global PasswordHash $wp_hasher PHPass object.
* @global PasswordHash $wp_hasher phpass object.
*
* @param string $password Plain text user password to hash.
* @return string The hash string of the password.
@ -2637,13 +2651,62 @@ if ( ! function_exists( 'wp_hash_password' ) ) :
) {
global $wp_hasher;
if ( empty( $wp_hasher ) ) {
require_once ABSPATH . WPINC . '/class-phpass.php';
// By default, use the portable hash from phpass.
$wp_hasher = new PasswordHash( 8, true );
if ( ! empty( $wp_hasher ) ) {
return $wp_hasher->HashPassword( trim( $password ) );
}
return $wp_hasher->HashPassword( trim( $password ) );
if ( strlen( $password ) > 4096 ) {
return '*';
}
/**
* Filters the hashing algorithm to use in the password_hash() and password_needs_rehash() functions.
*
* The default is the value of the `PASSWORD_BCRYPT` constant which means bcrypt is used.
*
* **Important:** The only password hashing algorithm that is guaranteed to be available across PHP
* installations is bcrypt. If you use any other algorithm you must make sure that it is available on
* the server. The `password_algos()` function can be used to check which hashing algorithms are available.
*
* The hashing options can be controlled via the {@see 'wp_hash_password_options'} filter.
*
* Other available constants include:
*
* - `PASSWORD_ARGON2I`
* - `PASSWORD_ARGON2ID`
* - `PASSWORD_DEFAULT`
*
* @since 6.8.0
*
* @param string $algorithm The hashing algorithm. Default is the value of the `PASSWORD_BCRYPT` constant.
*/
$algorithm = apply_filters( 'wp_hash_password_algorithm', PASSWORD_BCRYPT );
/**
* Filters the options passed to the password_hash() and password_needs_rehash() functions.
*
* The default hashing algorithm is bcrypt, but this can be changed via the {@see 'wp_hash_password_algorithm'}
* filter. You must ensure that the options are appropriate for the algorithm in use.
*
* @since 6.8.0
*
* @param array $options Array of options to pass to the password hashing functions.
* By default this is an empty array which means the default
* options will be used.
* @param string $algorithm The hashing algorithm in use.
*/
$options = apply_filters( 'wp_hash_password_options', array(), $algorithm );
// Algorithms other than bcrypt don't need to use pre-hashing.
if ( PASSWORD_BCRYPT !== $algorithm ) {
return password_hash( $password, $algorithm, $options );
}
// Use SHA-384 to retain entropy from a password that's longer than 72 bytes, and a `wp-sha384` key for domain separation.
$password_to_hash = base64_encode( hash_hmac( 'sha384', trim( $password ), 'wp-sha384', true ) );
// Add a prefix to facilitate distinguishing vanilla bcrypt hashes.
return '$wp' . password_hash( $password_to_hash, $algorithm, $options );
}
endif;
@ -2651,23 +2714,24 @@ if ( ! function_exists( 'wp_check_password' ) ) :
/**
* Checks a plaintext password against a hashed password.
*
* Maintains compatibility between old version and the new cookie authentication
* protocol using PHPass library. The $hash parameter is the encrypted password
* and the function compares the plain text password when encrypted similarly
* against the already encrypted password to see if they match.
* Note that this function may be used to check a value that is not a user password.
* A plugin may use this function to check a password of a different type, and there
* may not always be a user ID associated with the password.
*
* For integration with other applications, this function can be overwritten to
* instead use the other package password hashing algorithm.
*
* @since 2.5.0
* @since 6.8.0 Passwords in WordPress are now hashed with bcrypt by default. A
* password that wasn't hashed with bcrypt will be checked with phpass.
* Passwords hashed with md5 are no longer supported.
*
* @global PasswordHash $wp_hasher PHPass object used for checking the password
* against the $hash + $password.
* @uses PasswordHash::CheckPassword
* @global PasswordHash $wp_hasher phpass object. Used as a fallback for verifying
* passwords that were hashed with phpass.
*
* @param string $password Plaintext user's password.
* @param string $hash Hash of the user's password to check against.
* @param string|int $user_id Optional. User ID.
* @param string $password Plaintext password.
* @param string $hash Hash of the password to check against.
* @param string|int $user_id Optional. ID of a user associated with the password.
* @return bool False, if the $password does not match the hashed password.
*/
function wp_check_password(
@ -2678,45 +2742,107 @@ if ( ! function_exists( 'wp_check_password' ) ) :
) {
global $wp_hasher;
// If the hash is still md5...
if ( strlen( $hash ) <= 32 ) {
$check = hash_equals( $hash, md5( $password ) );
if ( $check && $user_id ) {
// Rehash using new hash.
wp_set_password( $password, $user_id );
$hash = wp_hash_password( $password );
}
$check = false;
// If the hash is still md5 or otherwise truncated then invalidate it.
if ( strlen( $hash ) <= 32 ) {
/**
* Filters whether the plaintext password matches the encrypted password.
* Filters whether the plaintext password matches the hashed password.
*
* @since 2.5.0
* @since 6.8.0 Passwords are now hashed with bcrypt by default.
* Old passwords may still be hashed with phpass.
*
* @param bool $check Whether the passwords match.
* @param string $password The plaintext password.
* @param string $hash The hashed password.
* @param string|int $user_id User ID. Can be empty.
* @param string|int $user_id Optional ID of a user associated with the password.
* Can be empty.
*/
return apply_filters( 'check_password', $check, $password, $hash, $user_id );
}
/*
* If the stored hash is longer than an MD5,
* presume the new style phpass portable hash.
*/
if ( empty( $wp_hasher ) ) {
if ( ! empty( $wp_hasher ) ) {
// Check the password using the overridden hasher.
$check = $wp_hasher->CheckPassword( $password, $hash );
} elseif ( strlen( $password ) > 4096 ) {
$check = false;
} elseif ( str_starts_with( $hash, '$wp' ) ) {
// Check the password using the current prefixed hash.
$password_to_verify = base64_encode( hash_hmac( 'sha384', $password, 'wp-sha384', true ) );
$check = password_verify( $password_to_verify, substr( $hash, 3 ) );
} elseif ( str_starts_with( $hash, '$P$' ) ) {
// Check the password using phpass.
require_once ABSPATH . WPINC . '/class-phpass.php';
// By default, use the portable hash from phpass.
$wp_hasher = new PasswordHash( 8, true );
$check = ( new PasswordHash( 8, true ) )->CheckPassword( $password, $hash );
} else {
// Check the password using compat support for any non-prefixed hash.
$check = password_verify( $password, $hash );
}
$check = $wp_hasher->CheckPassword( $password, $hash );
/** This filter is documented in wp-includes/pluggable.php */
return apply_filters( 'check_password', $check, $password, $hash, $user_id );
}
endif;
if ( ! function_exists( 'wp_password_needs_rehash' ) ) :
/**
* Checks whether a password hash needs to be rehashed.
*
* Passwords are hashed with bcrypt using the default cost. A password hashed in a prior version
* of WordPress may still be hashed with phpass and will need to be rehashed. If the default cost
* or algorithm is changed in PHP or WordPress then a password hashed in a previous version will
* need to be rehashed.
*
* Note that, just like wp_check_password(), this function may be used to check a value that is
* not a user password. A plugin may use this function to check a password of a different type,
* and there may not always be a user ID associated with the password.
*
* @since 6.8.0
*
* @global PasswordHash $wp_hasher phpass object.
*
* @param string $hash Hash of a password to check.
* @param string|int $user_id Optional. ID of a user associated with the password.
* @return bool Whether the hash needs to be rehashed.
*/
function wp_password_needs_rehash( $hash, $user_id = '' ) {
global $wp_hasher;
if ( ! empty( $wp_hasher ) ) {
return false;
}
/** This filter is documented in wp-includes/pluggable.php */
$algorithm = apply_filters( 'wp_hash_password_algorithm', PASSWORD_BCRYPT );
/** This filter is documented in wp-includes/pluggable.php */
$options = apply_filters( 'wp_hash_password_options', array(), $algorithm );
$prefixed = str_starts_with( $hash, '$wp' );
if ( ( PASSWORD_BCRYPT === $algorithm ) && ! $prefixed ) {
// If bcrypt is in use and the hash is not prefixed then it needs to be rehashed.
$needs_rehash = true;
} else {
// Otherwise check the hash minus its prefix if necessary.
$hash_to_check = $prefixed ? substr( $hash, 3 ) : $hash;
$needs_rehash = password_needs_rehash( $hash_to_check, $algorithm, $options );
}
/**
* Filters whether the password hash needs to be rehashed.
*
* @since 6.8.0
*
* @param bool $needs_rehash Whether the password hash needs to be rehashed.
* @param string $hash The password hash.
* @param string|int $user_id Optional. ID of a user associated with the password.
*/
return apply_filters( 'password_needs_rehash', $needs_rehash, $hash, $user_id );
}
endif;
if ( ! function_exists( 'wp_generate_password' ) ) :
/**
* Generates a random password drawn from the defined set of characters.
@ -2865,6 +2991,7 @@ if ( ! function_exists( 'wp_set_password' ) ) :
* of password resets if precautions are not taken to ensure it does not execute on every page load.
*
* @since 2.5.0
* @since 6.8.0 The password is now hashed using bcrypt by default instead of phpass.
*
* @global wpdb $wpdb WordPress database abstraction object.
*

View File

@ -205,7 +205,9 @@ function wp_authenticate_username_password(
return $user;
}
if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
$valid = wp_check_password( $password, $user->user_pass, $user->ID );
if ( ! $valid ) {
return new WP_Error(
'incorrect_password',
sprintf(
@ -219,6 +221,10 @@ function wp_authenticate_username_password(
);
}
if ( wp_password_needs_rehash( $user->user_pass, $user->ID ) ) {
wp_set_password( $password, $user->ID );
}
return $user;
}
@ -282,7 +288,9 @@ function wp_authenticate_email_password(
return $user;
}
if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
$valid = wp_check_password( $password, $user->user_pass, $user->ID );
if ( ! $valid ) {
return new WP_Error(
'incorrect_password',
sprintf(
@ -296,6 +304,10 @@ function wp_authenticate_email_password(
);
}
if ( wp_password_needs_rehash( $user->user_pass, $user->ID ) ) {
wp_set_password( $password, $user->ID );
}
return $user;
}
@ -445,7 +457,7 @@ function wp_authenticate_application_password(
$hashed_passwords = WP_Application_Passwords::get_user_application_passwords( $user->ID );
foreach ( $hashed_passwords as $key => $item ) {
if ( ! wp_check_password( $password, $item['password'], $user->ID ) ) {
if ( ! WP_Application_Passwords::check_password( $password, $item['password'] ) ) {
continue;
}
@ -2431,6 +2443,7 @@ function wp_insert_user( $userdata ) {
*
* @since 4.9.0
* @since 5.8.0 The `$userdata` parameter was added.
* @since 6.8.0 The user's password is now hashed using bcrypt instead of phpass.
*
* @param array $data {
* Values and keys for the user.
@ -2978,14 +2991,10 @@ function wp_get_password_hint() {
*
* @since 4.4.0
*
* @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
*
* @param WP_User $user User to retrieve password reset key for.
* @return string|WP_Error Password reset key on success. WP_Error on error.
*/
function get_password_reset_key( $user ) {
global $wp_hasher;
if ( ! ( $user instanceof WP_User ) ) {
return new WP_Error( 'invalidcombo', __( '<strong>Error:</strong> There is no account with that username or email address.' ) );
}
@ -3031,13 +3040,7 @@ function get_password_reset_key( $user ) {
*/
do_action( 'retrieve_password_key', $user->user_login, $key );
// Now insert the key, hashed, into the DB.
if ( empty( $wp_hasher ) ) {
require_once ABSPATH . WPINC . '/class-phpass.php';
$wp_hasher = new PasswordHash( 8, true );
}
$hashed = time() . ':' . $wp_hasher->HashPassword( $key );
$hashed = time() . ':' . wp_fast_hash( $key );
$key_saved = wp_update_user(
array(
@ -3063,9 +3066,7 @@ function get_password_reset_key( $user ) {
*
* @since 3.1.0
*
* @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
*
* @param string $key Hash to validate sending user's password.
* @param string $key The password reset key.
* @param string $login The user login.
* @return WP_User|WP_Error WP_User object on success, WP_Error object for invalid or expired keys.
*/
@ -3074,8 +3075,6 @@ function check_password_reset_key(
$key,
$login
) {
global $wp_hasher;
$key = preg_replace( '/[^a-z0-9]/i', '', $key );
if ( empty( $key ) || ! is_string( $key ) ) {
@ -3092,11 +3091,6 @@ function check_password_reset_key(
return new WP_Error( 'invalid_key', __( 'Invalid key.' ) );
}
if ( empty( $wp_hasher ) ) {
require_once ABSPATH . WPINC . '/class-phpass.php';
$wp_hasher = new PasswordHash( 8, true );
}
/**
* Filters the expiration time of password reset keys.
*
@ -3118,7 +3112,7 @@ function check_password_reset_key(
return new WP_Error( 'invalid_key', __( 'Invalid key.' ) );
}
$hash_is_correct = $wp_hasher->CheckPassword( $key, $pass_key );
$hash_is_correct = wp_verify_fast_hash( $key, $pass_key );
if ( $hash_is_correct && $expiration_time && time() < $expiration_time ) {
return $user;
@ -3133,7 +3127,7 @@ function check_password_reset_key(
/**
* Filters the return value of check_password_reset_key() when an
* old-style key is used.
* old-style key or an expired key is used.
*
* @since 3.7.0 Previously plain-text keys were stored in the database.
* @since 4.3.0 Previously key hashes were stored without an expiration time.
@ -3154,8 +3148,7 @@ function check_password_reset_key(
* @since 2.5.0
* @since 5.7.0 Added `$user_login` parameter.
*
* @global wpdb $wpdb WordPress database abstraction object.
* @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param string $user_login Optional. Username to send a password retrieval email for.
* Defaults to `$_POST['user_login']` if not set.
@ -4936,28 +4929,19 @@ All at ###SITENAME###
*
* @since 4.9.6
*
* @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
*
* @param int $request_id Request ID.
* @return string Confirmation key.
*/
function wp_generate_user_request_key( $request_id ) {
global $wp_hasher;
// Generate something random for a confirmation key.
$key = wp_generate_password( 20, false );
// Return the key, hashed.
if ( empty( $wp_hasher ) ) {
require_once ABSPATH . WPINC . '/class-phpass.php';
$wp_hasher = new PasswordHash( 8, true );
}
// Save the key, hashed.
wp_update_post(
array(
'ID' => $request_id,
'post_status' => 'request-pending',
'post_password' => $wp_hasher->HashPassword( $key ),
'post_password' => wp_fast_hash( $key ),
)
);
@ -4969,8 +4953,6 @@ function wp_generate_user_request_key( $request_id ) {
*
* @since 4.9.6
*
* @global PasswordHash $wp_hasher Portable PHP password hashing framework instance.
*
* @param string $request_id ID of the request being confirmed.
* @param string $key Provided key to validate.
* @return true|WP_Error True on success, WP_Error on failure.
@ -4980,8 +4962,6 @@ function wp_validate_user_request_key(
#[\SensitiveParameter]
$key
) {
global $wp_hasher;
$request_id = absint( $request_id );
$request = wp_get_user_request( $request_id );
$saved_key = $request->confirm_key;
@ -4999,11 +4979,6 @@ function wp_validate_user_request_key(
return new WP_Error( 'missing_key', __( 'The confirmation key is missing from this personal data request.' ) );
}
if ( empty( $wp_hasher ) ) {
require_once ABSPATH . WPINC . '/class-phpass.php';
$wp_hasher = new PasswordHash( 8, true );
}
/**
* Filters the expiration time of confirm keys.
*
@ -5014,7 +4989,7 @@ function wp_validate_user_request_key(
$expiration_duration = (int) apply_filters( 'user_request_key_expiration', DAY_IN_SECONDS );
$expiration_time = $key_request_time + $expiration_duration;
if ( ! $wp_hasher->CheckPassword( $key, $saved_key ) ) {
if ( ! wp_verify_fast_hash( $key, $saved_key ) ) {
return new WP_Error( 'invalid_key', __( 'The confirmation key is invalid for this personal data request.' ) );
}

View File

@ -329,6 +329,7 @@ require __DIR__ . '/spy-rest-server.php';
require __DIR__ . '/class-wp-rest-test-search-handler.php';
require __DIR__ . '/class-wp-rest-test-configurable-controller.php';
require __DIR__ . '/class-wp-fake-block-type.php';
require __DIR__ . '/class-wp-fake-hasher.php';
require __DIR__ . '/class-wp-sitemaps-test-provider.php';
require __DIR__ . '/class-wp-sitemaps-empty-test-provider.php';
require __DIR__ . '/class-wp-sitemaps-large-test-provider.php';

View File

@ -0,0 +1,41 @@
<?php
/**
* WP_Fake_Hasher for testing
*
* @package WordPress
* @since 6.8.0
*/
/**
* Test class.
*
* @since 6.8.0
*/
class WP_Fake_Hasher {
private $hash = '';
public function __construct() {
$this->hash = str_repeat( 'a', 36 );
}
/**
* Hashes a password.
*
* @param string $password Password to hash.
* @return string Hashed password.
*/
public function HashPassword( string $password ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
return $this->hash;
}
/**
* Checks the password hash.
*
* @param string $password Password to check.
* @param string $hash Hash to check against.
* @return bool Whether the password hash is valid.
*/
public function CheckPassword( string $password, string $hash ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
return $hash === $this->hash;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -217,6 +217,10 @@ class Tests_Pluggable_Signatures extends WP_UnitTestCase {
'hash',
'user_id' => '',
),
'wp_password_needs_rehash' => array(
'hash',
'user_id' => '',
),
'wp_generate_password' => array(
'length' => 12,
'special_chars' => true,

View File

@ -3,6 +3,10 @@
/**
* Tests for the PasswordHash external library.
*
* PasswordHash is no longer used to hash user passwords or security keys, but it is still used to
* hash post passwords and as a fallback to verify old passwords that were hashed by phpass. The
* library therefore needs to remain compatible with the latest versions of PHP.
*
* @covers PasswordHash
*/
class Tests_User_PasswordHash extends WP_UnitTestCase {