WordPress.org

Make WordPress Core

Changeset 33019


Ignore:
Timestamp:
07/01/2015 06:32:07 AM (6 years ago)
Author:
dd32
Message:

Expire password reset links after 24 hours (by default). This causes existing password reset links to become invalid.

Props markjaquith, voldemortensen, johnbillion, MikeHansenMe, dd32
See #32429

Location:
trunk
Files:
3 edited

Legend:

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

    r32980 r33019  
    24592459    }
    24602460
    2461     if ( $wp_hasher->CheckPassword( $key, $row->user_activation_key ) )
     2461    /**
     2462     * Filter the expiration time of password reset keys.
     2463     *
     2464     * @since 4.3.0
     2465     *
     2466     * @param int $expiration The expiration time in seconds.
     2467     */
     2468    $expiration_duration = apply_filters( 'password_reset_expiration', DAY_IN_SECONDS );
     2469
     2470    if ( false !== strpos( $row->user_activation_key, ':' ) ) {
     2471        list( $pass_request_time, $pass_key ) = explode( ':', $row->user_activation_key, 2 );
     2472        $expiration_time = $pass_request_time + $expiration_duration;
     2473    } else {
     2474        $pass_key = $row->user_activation_key;
     2475        $expiration_time = false;
     2476    }
     2477
     2478    $hash_is_correct = $wp_hasher->CheckPassword( $key, $pass_key );
     2479
     2480    if ( $hash_is_correct && $expiration_time && time() < $expiration_time ) {
    24622481        return get_userdata( $row->ID );
    2463 
    2464     if ( $key === $row->user_activation_key ) {
     2482    } elseif ( $hash_is_correct && $expiration_time ) {
     2483        // Key has an expiration time that's passed
     2484        return new WP_Error( 'expired_key', __( 'Invalid key' ) );
     2485    }
     2486
     2487    if ( hash_equals( $row->user_activation_key, $key ) || ( $hash_is_correct && ! $expiration_time ) ) {
    24652488        $return = new WP_Error( 'expired_key', __( 'Invalid key' ) );
    24662489        $user_id = $row->ID;
     
    24682491        /**
    24692492         * Filter the return value of check_password_reset_key() when an
    2470          * old-style key is used (plain-text key was stored in the database).
     2493         * old-style key is used.
    24712494         *
    2472          * @since 3.7.0
     2495         * @since 3.7.0 Previously plain-text keys were stored in the database.
     2496         * @since 4.3.0 Previously key hashes were stored without an expiration time.
    24732497         *
    24742498         * @param WP_Error $return  A WP_Error object denoting an expired key.
  • trunk/src/wp-login.php

    r32672 r33019  
    364364        $wp_hasher = new PasswordHash( 8, true );
    365365    }
    366     $hashed = $wp_hasher->HashPassword( $key );
     366    $hashed = time() . ':' . $wp_hasher->HashPassword( $key );
    367367    $wpdb->update( $wpdb->users, array( 'user_activation_key' => $hashed ), array( 'user_login' => $user_login ) );
    368368
     
    529529
    530530    if ( isset( $_GET['error'] ) ) {
    531         if ( 'invalidkey' == $_GET['error'] )
    532             $errors->add( 'invalidkey', __( 'Sorry, that key does not appear to be valid.' ) );
    533         elseif ( 'expiredkey' == $_GET['error'] )
    534             $errors->add( 'expiredkey', __( 'Sorry, that key has expired. Please try again.' ) );
     531        if ( 'invalidkey' == $_GET['error'] ) {
     532            $errors->add( 'invalidkey', __( 'Your password reset link appears to be invalid. Please request a new lnk below.' ) );
     533        } elseif ( 'expiredkey' == $_GET['error'] ) {
     534            $errors->add( 'expiredkey', __( 'Your password reset link has expired. Please request a new link below.' ) );
     535        }
    535536    }
    536537
  • trunk/tests/phpunit/tests/auth.php

    r32785 r33019  
    77class Tests_Auth extends WP_UnitTestCase {
    88    var $user_id;
     9    var $wp_hasher;
    910
    1011    function setUp() {
    1112        parent::setUp();
    1213        $this->user_id = $this->factory->user->create();
     14
     15        require_once ABSPATH . WPINC . '/class-phpass.php';
     16        $this->wp_hasher = new PasswordHash( 8, true );
    1317    }
    1418
     
    160164        $this->assertInstanceOf( 'WP_Error', $user );
    161165    }
     166
     167    /**
     168     * @ticket 32429
     169     */
     170    function test_user_activation_key_is_checked() {
     171        global $wpdb;
     172
     173        $key  = wp_generate_password( 20, false );
     174        $user = $this->factory->user->create_and_get();
     175        $wpdb->update( $wpdb->users, array(
     176            'user_activation_key' => strtotime( '-1 hour' ) . ':' . $this->wp_hasher->HashPassword( $key ),
     177        ), array(
     178            'ID' => $user->ID,
     179        ) );
     180
     181        // A valid key should be accepted
     182        $check = check_password_reset_key( $key, $user->user_login );
     183        $this->assertInstanceOf( 'WP_User', $check );
     184        $this->assertSame( $user->ID, $check->ID );
     185
     186        // An invalid key should be rejected
     187        $check = check_password_reset_key( 'key', $user->user_login );
     188        $this->assertInstanceOf( 'WP_Error', $check );
     189
     190        // An empty key should be rejected
     191        $check = check_password_reset_key( '', $user->user_login );
     192        $this->assertInstanceOf( 'WP_Error', $check );
     193
     194        // A truncated key should be rejected
     195        $partial = substr( $key, 0, 10 );
     196        $check = check_password_reset_key( $partial, $user->user_login );
     197        $this->assertInstanceOf( 'WP_Error', $check );
     198    }
     199
     200    /**
     201     * @ticket 32429
     202     */
     203    function test_expired_user_activation_key_is_rejected() {
     204        global $wpdb;
     205
     206        $key  = wp_generate_password( 20, false );
     207        $user = $this->factory->user->create_and_get();
     208        $wpdb->update( $wpdb->users, array(
     209            'user_activation_key' => strtotime( '-48 hours' ) . ':' . $this->wp_hasher->HashPassword( $key ),
     210        ), array(
     211            'ID' => $user->ID,
     212        ) );
     213
     214        // An expired but otherwise valid key should be rejected
     215        $check = check_password_reset_key( $key, $user->user_login );
     216        $this->assertInstanceOf( 'WP_Error', $check );
     217    }
     218
     219    /**
     220     * @ticket 32429
     221     */
     222    function test_empty_user_activation_key_fails_key_check() {
     223        global $wpdb;
     224
     225        $user = $this->factory->user->create_and_get();
     226
     227        // An empty user_activation_key should not allow any key to be accepted
     228        $check = check_password_reset_key( 'key', $user->user_login );
     229        $this->assertInstanceOf( 'WP_Error', $check );
     230
     231        // An empty user_activation_key should not allow an empty key to be accepted
     232        $check = check_password_reset_key( '', $user->user_login );
     233        $this->assertInstanceOf( 'WP_Error', $check );
     234    }
     235
     236    /**
     237     * @ticket 32429
     238     */
     239    function test_legacy_user_activation_key_is_rejected() {
     240        global $wpdb;
     241
     242        // A legacy user_activation_key is one without the `time()` prefix introduced in WordPress 4.3.
     243
     244        $key  = wp_generate_password( 20, false );
     245        $user = $this->factory->user->create_and_get();
     246        $wpdb->update( $wpdb->users, array(
     247            'user_activation_key' => $this->wp_hasher->HashPassword( $key ),
     248        ), array(
     249            'ID' => $user->ID,
     250        ) );
     251
     252        // A legacy user_activation_key should not be accepted
     253        $check = check_password_reset_key( $key, $user->user_login );
     254        $this->assertInstanceOf( 'WP_Error', $check );
     255
     256        // An empty key with a legacy user_activation_key should be rejected
     257        $check = check_password_reset_key( '', $user->user_login );
     258        $this->assertInstanceOf( 'WP_Error', $check );
     259    }
     260
     261    /**
     262     * @ticket 32429
     263     * @ticket 24783
     264     */
     265    function test_plaintext_user_activation_key_is_rejected() {
     266        global $wpdb;
     267
     268        // A plaintext user_activation_key is one stored before hashing was introduced in WordPress 3.7.
     269
     270        $key  = wp_generate_password( 20, false );
     271        $user = $this->factory->user->create_and_get();
     272        $wpdb->update( $wpdb->users, array(
     273            'user_activation_key' => $key,
     274        ), array(
     275            'ID' => $user->ID,
     276        ) );
     277
     278        // A plaintext user_activation_key should not allow an otherwise valid key to be accepted
     279        $check = check_password_reset_key( $key, $user->user_login );
     280        $this->assertInstanceOf( 'WP_Error', $check );
     281
     282        // A plaintext user_activation_key should not allow an empty key to be accepted
     283        $check = check_password_reset_key( '', $user->user_login );
     284        $this->assertInstanceOf( 'WP_Error', $check );
     285    }
    162286}
Note: See TracChangeset for help on using the changeset viewer.