Skip to content

Commit 360732e

Browse files
committed
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
1 parent bcaaa16 commit 360732e

File tree

2 files changed

+87
-26
lines changed

2 files changed

+87
-26
lines changed

src/wp-includes/pluggable.php

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2724,7 +2724,6 @@ function wp_hash_password(
27242724
* @since 2.5.0
27252725
* @since 6.8.0 Passwords in WordPress are now hashed with bcrypt by default. A
27262726
* password that wasn't hashed with bcrypt will be checked with phpass.
2727-
* Passwords hashed with md5 are no longer supported.
27282727
*
27292728
* @global PasswordHash $wp_hasher phpass object. Used as a fallback for verifying
27302729
* passwords that were hashed with phpass.
@@ -2742,30 +2741,14 @@ function wp_check_password(
27422741
) {
27432742
global $wp_hasher;
27442743

2745-
$check = false;
2746-
2747-
// If the hash is still md5 or otherwise truncated then invalidate it.
27482744
if ( strlen( $hash ) <= 32 ) {
2749-
/**
2750-
* Filters whether the plaintext password matches the hashed password.
2751-
*
2752-
* @since 2.5.0
2753-
* @since 6.8.0 Passwords are now hashed with bcrypt by default.
2754-
* Old passwords may still be hashed with phpass.
2755-
*
2756-
* @param bool $check Whether the passwords match.
2757-
* @param string $password The plaintext password.
2758-
* @param string $hash The hashed password.
2759-
* @param string|int $user_id Optional ID of a user associated with the password.
2760-
* Can be empty.
2761-
*/
2762-
return apply_filters( 'check_password', $check, $password, $hash, $user_id );
2763-
}
2764-
2765-
if ( ! empty( $wp_hasher ) ) {
2745+
// Check the hash using md5 regardless of the current hashing mechanism.
2746+
$check = hash_equals( $hash, md5( $password ) );
2747+
} elseif ( ! empty( $wp_hasher ) ) {
27662748
// Check the password using the overridden hasher.
27672749
$check = $wp_hasher->CheckPassword( $password, $hash );
27682750
} elseif ( strlen( $password ) > 4096 ) {
2751+
// Passwords longer than 4096 characters are not supported.
27692752
$check = false;
27702753
} elseif ( str_starts_with( $hash, '$wp' ) ) {
27712754
// Check the password using the current prefixed hash.
@@ -2780,7 +2763,19 @@ function wp_check_password(
27802763
$check = password_verify( $password, $hash );
27812764
}
27822765

2783-
/** This filter is documented in wp-includes/pluggable.php */
2766+
/**
2767+
* Filters whether the plaintext password matches the hashed password.
2768+
*
2769+
* @since 2.5.0
2770+
* @since 6.8.0 Passwords are now hashed with bcrypt by default.
2771+
* Old passwords may still be hashed with phpass or md5.
2772+
*
2773+
* @param bool $check Whether the passwords match.
2774+
* @param string $password The plaintext password.
2775+
* @param string $hash The hashed password.
2776+
* @param string|int $user_id Optional ID of a user associated with the password.
2777+
* Can be empty.
2778+
*/
27842779
return apply_filters( 'check_password', $check, $password, $hash, $user_id );
27852780
}
27862781
endif;

tests/phpunit/tests/auth.php

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -318,10 +318,10 @@ public function test_wp_check_password_supports_argon2id_hash() {
318318
/**
319319
* @ticket 21022
320320
*/
321-
public function test_wp_check_password_does_not_support_md5_hashes() {
321+
public function test_wp_check_password_supports_md5_hash() {
322322
$password = 'password';
323323
$hash = md5( $password );
324-
$this->assertFalse( wp_check_password( $password, $hash ) );
324+
$this->assertTrue( wp_check_password( $password, $hash ) );
325325
$this->assertSame( 1, did_filter( 'check_password' ) );
326326
}
327327

@@ -363,8 +363,6 @@ public function test_wp_check_password_does_not_support_empty_password( $value )
363363

364364
public function data_empty_values() {
365365
return array(
366-
// Integer zero:
367-
array( 0 ),
368366
// String zero:
369367
array( '0' ),
370368
// Zero-length string:
@@ -1079,6 +1077,42 @@ public function test_phpass_password_is_rehashed_after_successful_user_password_
10791077
$this->assertSame( self::$user_id, $user->ID );
10801078
}
10811079

1080+
/**
1081+
* @dataProvider data_usernames
1082+
*
1083+
* @ticket 21022
1084+
*/
1085+
public function test_md5_password_is_rehashed_after_successful_user_password_authentication( $username_or_email ) {
1086+
$password = 'password';
1087+
1088+
// Set the user password with the old md5 algorithm.
1089+
self::set_user_password_with_md5( $password, self::$user_id );
1090+
1091+
// Verify that the password needs rehashing.
1092+
$hash = get_userdata( self::$user_id )->user_pass;
1093+
$this->assertTrue( wp_password_needs_rehash( $hash, self::$user_id ) );
1094+
1095+
// Authenticate.
1096+
$user = wp_authenticate( $username_or_email, $password );
1097+
1098+
// Verify that the md5 password hash was valid.
1099+
$this->assertNotWPError( $user );
1100+
$this->assertInstanceOf( 'WP_User', $user );
1101+
$this->assertSame( self::$user_id, $user->ID );
1102+
1103+
// Verify that the password no longer needs rehashing.
1104+
$hash = get_userdata( self::$user_id )->user_pass;
1105+
$this->assertFalse( wp_password_needs_rehash( $hash, self::$user_id ) );
1106+
1107+
// Authenticate a second time to ensure the new hash is valid.
1108+
$user = wp_authenticate( $username_or_email, $password );
1109+
1110+
// Verify that the bcrypt password hash is valid.
1111+
$this->assertNotWPError( $user );
1112+
$this->assertInstanceOf( 'WP_User', $user );
1113+
$this->assertSame( self::$user_id, $user->ID );
1114+
}
1115+
10821116
/**
10831117
* @dataProvider data_usernames
10841118
*
@@ -1772,6 +1806,38 @@ private static function set_user_password_with_phpass( string $password, int $us
17721806
clean_user_cache( $user_id );
17731807
}
17741808

1809+
/**
1810+
* Test the tests
1811+
*
1812+
* @covers Tests_Auth::set_user_password_with_md5
1813+
*
1814+
* @ticket 21022
1815+
*/
1816+
public function test_set_user_password_with_md5() {
1817+
$password = 'password';
1818+
1819+
// Set the user password with the old md5 algorithm.
1820+
self::set_user_password_with_md5( $password, self::$user_id );
1821+
1822+
// Ensure the password is hashed with md5.
1823+
$hash = get_userdata( self::$user_id )->user_pass;
1824+
$this->assertSame( md5( $password ), $hash );
1825+
}
1826+
1827+
private static function set_user_password_with_md5( string $password, int $user_id ) {
1828+
global $wpdb;
1829+
1830+
$wpdb->update(
1831+
$wpdb->users,
1832+
array(
1833+
'user_pass' => md5( $password ),
1834+
),
1835+
array(
1836+
'ID' => $user_id,
1837+
)
1838+
);
1839+
clean_user_cache( $user_id );
1840+
}
17751841

17761842
/**
17771843
* Test the tests

0 commit comments

Comments
 (0)