Make WordPress Core

Changeset 59893


Ignore:
Timestamp:
02/28/2025 06:51:44 PM (2 months ago)
Author:
johnbillion
Message:

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.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/pluggable.php

    r59858 r59893  
    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
     
    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' ) ) {
     
    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    }
  • trunk/tests/phpunit/tests/auth.php

    r59828 r59893  
    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    }
     
    364364    public function data_empty_values() {
    365365        return array(
    366             // Integer zero:
    367             array( 0 ),
    368366            // String zero:
    369367            array( '0' ),
     
    10851083     * @ticket 21022
    10861084     */
     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
     1116    /**
     1117     * @dataProvider data_usernames
     1118     *
     1119     * @ticket 21022
     1120     */
    10871121    public function test_bcrypt_password_is_rehashed_with_new_cost_after_successful_user_password_authentication( $username_or_email ) {
    10881122        $password = 'password';
     
    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    /**
Note: See TracChangeset for help on using the changeset viewer.