Security: Reintroduce support for passwords hashed with MD5.

This reinstates the ability for a user to log in to an account where the password is hashed using MD5. This means that the ability to reset a password directly in the database using an SQL query or a database administration tool will be retained without the need to implement or integrate with bcrypt or phpass.

A password hashed with MD5 will get upgraded to bcrypt at the point where a user successfully logs in, just as is the case with a phpass hash.

Props audrasjb, aaronjorbin, johnbillion, david-innes, benniledl.

See #21022.

git-svn-id: https://develop.svn.wordpress.org/trunk@59893 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
John Blackbourn 2025-02-28 18:51:44 +00:00
parent bcaaa16a1f
commit 360732e132
2 changed files with 87 additions and 26 deletions

View File

@ -2724,7 +2724,6 @@ if ( ! function_exists( 'wp_check_password' ) ) :
* @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 as a fallback for verifying
* passwords that were hashed with phpass.
@ -2742,30 +2741,14 @@ if ( ! function_exists( 'wp_check_password' ) ) :
) {
global $wp_hasher;
$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 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 Optional ID of a user associated with the password.
* Can be empty.
*/
return apply_filters( 'check_password', $check, $password, $hash, $user_id );
}
if ( ! empty( $wp_hasher ) ) {
// Check the hash using md5 regardless of the current hashing mechanism.
$check = hash_equals( $hash, md5( $password ) );
} elseif ( ! empty( $wp_hasher ) ) {
// Check the password using the overridden hasher.
$check = $wp_hasher->CheckPassword( $password, $hash );
} elseif ( strlen( $password ) > 4096 ) {
// Passwords longer than 4096 characters are not supported.
$check = false;
} elseif ( str_starts_with( $hash, '$wp' ) ) {
// Check the password using the current prefixed hash.
@ -2780,7 +2763,19 @@ if ( ! function_exists( 'wp_check_password' ) ) :
$check = password_verify( $password, $hash );
}
/** This filter is documented in wp-includes/pluggable.php */
/**
* 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 or md5.
*
* @param bool $check Whether the passwords match.
* @param string $password The plaintext password.
* @param string $hash The hashed password.
* @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 );
}
endif;

View File

@ -318,10 +318,10 @@ class Tests_Auth extends WP_UnitTestCase {
/**
* @ticket 21022
*/
public function test_wp_check_password_does_not_support_md5_hashes() {
public function test_wp_check_password_supports_md5_hash() {
$password = 'password';
$hash = md5( $password );
$this->assertFalse( wp_check_password( $password, $hash ) );
$this->assertTrue( wp_check_password( $password, $hash ) );
$this->assertSame( 1, did_filter( 'check_password' ) );
}
@ -363,8 +363,6 @@ class Tests_Auth extends WP_UnitTestCase {
public function data_empty_values() {
return array(
// Integer zero:
array( 0 ),
// String zero:
array( '0' ),
// Zero-length string:
@ -1079,6 +1077,42 @@ class Tests_Auth extends WP_UnitTestCase {
$this->assertSame( self::$user_id, $user->ID );
}
/**
* @dataProvider data_usernames
*
* @ticket 21022
*/
public function test_md5_password_is_rehashed_after_successful_user_password_authentication( $username_or_email ) {
$password = 'password';
// Set the user password with the old md5 algorithm.
self::set_user_password_with_md5( $password, self::$user_id );
// Verify that the password needs rehashing.
$hash = get_userdata( self::$user_id )->user_pass;
$this->assertTrue( wp_password_needs_rehash( $hash, self::$user_id ) );
// Authenticate.
$user = wp_authenticate( $username_or_email, $password );
// Verify that the md5 password hash was valid.
$this->assertNotWPError( $user );
$this->assertInstanceOf( 'WP_User', $user );
$this->assertSame( self::$user_id, $user->ID );
// Verify that the password no longer needs rehashing.
$hash = get_userdata( self::$user_id )->user_pass;
$this->assertFalse( wp_password_needs_rehash( $hash, self::$user_id ) );
// Authenticate a second time to ensure the new hash is valid.
$user = wp_authenticate( $username_or_email, $password );
// Verify that the bcrypt password hash is valid.
$this->assertNotWPError( $user );
$this->assertInstanceOf( 'WP_User', $user );
$this->assertSame( self::$user_id, $user->ID );
}
/**
* @dataProvider data_usernames
*
@ -1772,6 +1806,38 @@ class Tests_Auth extends WP_UnitTestCase {
clean_user_cache( $user_id );
}
/**
* Test the tests
*
* @covers Tests_Auth::set_user_password_with_md5
*
* @ticket 21022
*/
public function test_set_user_password_with_md5() {
$password = 'password';
// Set the user password with the old md5 algorithm.
self::set_user_password_with_md5( $password, self::$user_id );
// Ensure the password is hashed with md5.
$hash = get_userdata( self::$user_id )->user_pass;
$this->assertSame( md5( $password ), $hash );
}
private static function set_user_password_with_md5( string $password, int $user_id ) {
global $wpdb;
$wpdb->update(
$wpdb->users,
array(
'user_pass' => md5( $password ),
),
array(
'ID' => $user_id,
)
);
clean_user_cache( $user_id );
}
/**
* Test the tests