Make WordPress Core


Ignore:
Timestamp:
02/17/2025 11:22:33 AM (3 months ago)
Author:
johnbillion
Message:

Security: Switch to using bcrypt for hashing user passwords and BLAKE2b for hashing application passwords and security keys.

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

File:
1 edited

Legend:

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

    r59803 r59828  
    694694     * @param string $cookie Optional. If used, will validate contents instead of cookie's.
    695695     * @param string $scheme Optional. The cookie scheme to use: 'auth', 'secure_auth', or 'logged_in'.
     696     *                       Note: This does *not* default to 'auth' like other cookie functions.
    696697     * @return int|false User ID if valid cookie, false if invalid.
    697698     */
     
    769770        }
    770771
    771         $pass_frag = substr( $user->user_pass, 8, 4 );
     772        if ( str_starts_with( $user->user_pass, '$P$' ) || str_starts_with( $user->user_pass, '$2y$' ) ) {
     773            // Retain previous behaviour of phpass or vanilla bcrypt hashed passwords.
     774            $pass_frag = substr( $user->user_pass, 8, 4 );
     775        } else {
     776            // Otherwise, use a substring from the end of the hash to avoid dealing with potentially long hash prefixes.
     777            $pass_frag = substr( $user->user_pass, -4 );
     778        }
    772779
    773780        $key = wp_hash( $username . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme );
     
    870877        }
    871878
    872         $pass_frag = substr( $user->user_pass, 8, 4 );
     879        if ( str_starts_with( $user->user_pass, '$P$' ) || str_starts_with( $user->user_pass, '$2y$' ) ) {
     880            // Retain previous behaviour of phpass or vanilla bcrypt hashed passwords.
     881            $pass_frag = substr( $user->user_pass, 8, 4 );
     882        } else {
     883            // Otherwise, use a substring from the end of the hash to avoid dealing with potentially long hash prefixes.
     884            $pass_frag = substr( $user->user_pass, -4 );
     885        }
    873886
    874887        $key = wp_hash( $user->user_login . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme );
     
    26262639     *
    26272640     * @since 2.5.0
    2628      *
    2629      * @global PasswordHash $wp_hasher PHPass object.
     2641     * @since 6.8.0 The password is now hashed using bcrypt by default instead of phpass.
     2642     *
     2643     * @global PasswordHash $wp_hasher phpass object.
    26302644     *
    26312645     * @param string $password Plain text user password to hash.
     
    26382652        global $wp_hasher;
    26392653
    2640         if ( empty( $wp_hasher ) ) {
    2641             require_once ABSPATH . WPINC . '/class-phpass.php';
    2642             // By default, use the portable hash from phpass.
    2643             $wp_hasher = new PasswordHash( 8, true );
    2644         }
    2645 
    2646         return $wp_hasher->HashPassword( trim( $password ) );
     2654        if ( ! empty( $wp_hasher ) ) {
     2655            return $wp_hasher->HashPassword( trim( $password ) );
     2656        }
     2657
     2658        if ( strlen( $password ) > 4096 ) {
     2659            return '*';
     2660        }
     2661
     2662        /**
     2663         * Filters the hashing algorithm to use in the password_hash() and password_needs_rehash() functions.
     2664         *
     2665         * The default is the value of the `PASSWORD_BCRYPT` constant which means bcrypt is used.
     2666         *
     2667         * **Important:** The only password hashing algorithm that is guaranteed to be available across PHP
     2668         * installations is bcrypt. If you use any other algorithm you must make sure that it is available on
     2669         * the server. The `password_algos()` function can be used to check which hashing algorithms are available.
     2670         *
     2671         * The hashing options can be controlled via the {@see 'wp_hash_password_options'} filter.
     2672         *
     2673         * Other available constants include:
     2674         *
     2675         * - `PASSWORD_ARGON2I`
     2676         * - `PASSWORD_ARGON2ID`
     2677         * - `PASSWORD_DEFAULT`
     2678         *
     2679         * @since 6.8.0
     2680         *
     2681         * @param string $algorithm The hashing algorithm. Default is the value of the `PASSWORD_BCRYPT` constant.
     2682         */
     2683        $algorithm = apply_filters( 'wp_hash_password_algorithm', PASSWORD_BCRYPT );
     2684
     2685        /**
     2686         * Filters the options passed to the password_hash() and password_needs_rehash() functions.
     2687         *
     2688         * The default hashing algorithm is bcrypt, but this can be changed via the {@see 'wp_hash_password_algorithm'}
     2689         * filter. You must ensure that the options are appropriate for the algorithm in use.
     2690         *
     2691         * @since 6.8.0
     2692         *
     2693         * @param array $options    Array of options to pass to the password hashing functions.
     2694         *                          By default this is an empty array which means the default
     2695         *                          options will be used.
     2696         * @param string $algorithm The hashing algorithm in use.
     2697         */
     2698        $options = apply_filters( 'wp_hash_password_options', array(), $algorithm );
     2699
     2700        // Algorithms other than bcrypt don't need to use pre-hashing.
     2701        if ( PASSWORD_BCRYPT !== $algorithm ) {
     2702            return password_hash( $password, $algorithm, $options );
     2703        }
     2704
     2705        // Use SHA-384 to retain entropy from a password that's longer than 72 bytes, and a `wp-sha384` key for domain separation.
     2706        $password_to_hash = base64_encode( hash_hmac( 'sha384', trim( $password ), 'wp-sha384', true ) );
     2707
     2708        // Add a prefix to facilitate distinguishing vanilla bcrypt hashes.
     2709        return '$wp' . password_hash( $password_to_hash, $algorithm, $options );
    26472710    }
    26482711endif;
     
    26522715     * Checks a plaintext password against a hashed password.
    26532716     *
    2654      * Maintains compatibility between old version and the new cookie authentication
    2655      * protocol using PHPass library. The $hash parameter is the encrypted password
    2656      * and the function compares the plain text password when encrypted similarly
    2657      * against the already encrypted password to see if they match.
     2717     * Note that this function may be used to check a value that is not a user password.
     2718     * A plugin may use this function to check a password of a different type, and there
     2719     * may not always be a user ID associated with the password.
    26582720     *
    26592721     * For integration with other applications, this function can be overwritten to
     
    26612723     *
    26622724     * @since 2.5.0
    2663      *
    2664      * @global PasswordHash $wp_hasher PHPass object used for checking the password
    2665      *                                 against the $hash + $password.
    2666      * @uses PasswordHash::CheckPassword
    2667      *
    2668      * @param string     $password Plaintext user's password.
    2669      * @param string     $hash     Hash of the user's password to check against.
    2670      * @param string|int $user_id  Optional. User ID.
     2725     * @since 6.8.0 Passwords in WordPress are now hashed with bcrypt by default. A
     2726     *              password that wasn't hashed with bcrypt will be checked with phpass.
     2727     *              Passwords hashed with md5 are no longer supported.
     2728     *
     2729     * @global PasswordHash $wp_hasher phpass object. Used as a fallback for verifying
     2730     *                                 passwords that were hashed with phpass.
     2731     *
     2732     * @param string     $password Plaintext password.
     2733     * @param string     $hash     Hash of the password to check against.
     2734     * @param string|int $user_id  Optional. ID of a user associated with the password.
    26712735     * @return bool False, if the $password does not match the hashed password.
    26722736     */
     
    26792743        global $wp_hasher;
    26802744
    2681         // If the hash is still md5...
     2745        $check = false;
     2746
     2747        // If the hash is still md5 or otherwise truncated then invalidate it.
    26822748        if ( strlen( $hash ) <= 32 ) {
    2683             $check = hash_equals( $hash, md5( $password ) );
    2684             if ( $check && $user_id ) {
    2685                 // Rehash using new hash.
    2686                 wp_set_password( $password, $user_id );
    2687                 $hash = wp_hash_password( $password );
    2688             }
    2689 
    26902749            /**
    2691              * Filters whether the plaintext password matches the encrypted password.
     2750             * Filters whether the plaintext password matches the hashed password.
    26922751             *
    26932752             * @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.
    26942755             *
    26952756             * @param bool       $check    Whether the passwords match.
    26962757             * @param string     $password The plaintext password.
    26972758             * @param string     $hash     The hashed password.
    2698              * @param string|int $user_id  User ID. Can be empty.
     2759             * @param string|int $user_id  Optional ID of a user associated with the password.
     2760             *                             Can be empty.
    26992761             */
    27002762            return apply_filters( 'check_password', $check, $password, $hash, $user_id );
    27012763        }
    27022764
    2703         /*
    2704          * If the stored hash is longer than an MD5,
    2705          * presume the new style phpass portable hash.
    2706          */
    2707         if ( empty( $wp_hasher ) ) {
     2765        if ( ! empty( $wp_hasher ) ) {
     2766            // Check the password using the overridden hasher.
     2767            $check = $wp_hasher->CheckPassword( $password, $hash );
     2768        } elseif ( strlen( $password ) > 4096 ) {
     2769            $check = false;
     2770        } elseif ( str_starts_with( $hash, '$wp' ) ) {
     2771            // Check the password using the current prefixed hash.
     2772            $password_to_verify = base64_encode( hash_hmac( 'sha384', $password, 'wp-sha384', true ) );
     2773            $check              = password_verify( $password_to_verify, substr( $hash, 3 ) );
     2774        } elseif ( str_starts_with( $hash, '$P$' ) ) {
     2775            // Check the password using phpass.
    27082776            require_once ABSPATH . WPINC . '/class-phpass.php';
    2709             // By default, use the portable hash from phpass.
    2710             $wp_hasher = new PasswordHash( 8, true );
    2711         }
    2712 
    2713         $check = $wp_hasher->CheckPassword( $password, $hash );
     2777            $check = ( new PasswordHash( 8, true ) )->CheckPassword( $password, $hash );
     2778        } else {
     2779            // Check the password using compat support for any non-prefixed hash.
     2780            $check = password_verify( $password, $hash );
     2781        }
    27142782
    27152783        /** This filter is documented in wp-includes/pluggable.php */
    27162784        return apply_filters( 'check_password', $check, $password, $hash, $user_id );
     2785    }
     2786endif;
     2787
     2788if ( ! function_exists( 'wp_password_needs_rehash' ) ) :
     2789    /**
     2790     * Checks whether a password hash needs to be rehashed.
     2791     *
     2792     * Passwords are hashed with bcrypt using the default cost. A password hashed in a prior version
     2793     * of WordPress may still be hashed with phpass and will need to be rehashed. If the default cost
     2794     * or algorithm is changed in PHP or WordPress then a password hashed in a previous version will
     2795     * need to be rehashed.
     2796     *
     2797     * Note that, just like wp_check_password(), this function may be used to check a value that is
     2798     * not a user password. A plugin may use this function to check a password of a different type,
     2799     * and there may not always be a user ID associated with the password.
     2800     *
     2801     * @since 6.8.0
     2802     *
     2803     * @global PasswordHash $wp_hasher phpass object.
     2804     *
     2805     * @param string     $hash    Hash of a password to check.
     2806     * @param string|int $user_id Optional. ID of a user associated with the password.
     2807     * @return bool Whether the hash needs to be rehashed.
     2808     */
     2809    function wp_password_needs_rehash( $hash, $user_id = '' ) {
     2810        global $wp_hasher;
     2811
     2812        if ( ! empty( $wp_hasher ) ) {
     2813            return false;
     2814        }
     2815
     2816        /** This filter is documented in wp-includes/pluggable.php */
     2817        $algorithm = apply_filters( 'wp_hash_password_algorithm', PASSWORD_BCRYPT );
     2818
     2819        /** This filter is documented in wp-includes/pluggable.php */
     2820        $options = apply_filters( 'wp_hash_password_options', array(), $algorithm );
     2821
     2822        $prefixed = str_starts_with( $hash, '$wp' );
     2823
     2824        if ( ( PASSWORD_BCRYPT === $algorithm ) && ! $prefixed ) {
     2825            // If bcrypt is in use and the hash is not prefixed then it needs to be rehashed.
     2826            $needs_rehash = true;
     2827        } else {
     2828            // Otherwise check the hash minus its prefix if necessary.
     2829            $hash_to_check = $prefixed ? substr( $hash, 3 ) : $hash;
     2830            $needs_rehash  = password_needs_rehash( $hash_to_check, $algorithm, $options );
     2831        }
     2832
     2833        /**
     2834         * Filters whether the password hash needs to be rehashed.
     2835         *
     2836         * @since 6.8.0
     2837         *
     2838         * @param bool       $needs_rehash Whether the password hash needs to be rehashed.
     2839         * @param string     $hash         The password hash.
     2840         * @param string|int $user_id      Optional. ID of a user associated with the password.
     2841         */
     2842        return apply_filters( 'password_needs_rehash', $needs_rehash, $hash, $user_id );
    27172843    }
    27182844endif;
     
    28662992     *
    28672993     * @since 2.5.0
     2994     * @since 6.8.0 The password is now hashed using bcrypt by default instead of phpass.
    28682995     *
    28692996     * @global wpdb $wpdb WordPress database abstraction object.
Note: See TracChangeset for help on using the changeset viewer.