diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index 3a6190beff..e7ce2edb41 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -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; diff --git a/tests/phpunit/tests/auth.php b/tests/phpunit/tests/auth.php index 309ea7fba1..6c22b89855 100644 --- a/tests/phpunit/tests/auth.php +++ b/tests/phpunit/tests/auth.php @@ -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