diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index a88ffcb66b..9a60cd2069 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -1587,29 +1587,58 @@ function _wp_get_user_contactmethods( $user = null ) { /** * Retrieves a user row based on password reset key and login * + * A key is considered 'expired' if it exactly matches the value of the + * user_activation_key field, rather than being matched after going through the + * hashing process. This field is now hashed; old values are no longer accepted + * but have a different WP_Error code so good user feedback can be provided. + * * @uses $wpdb WordPress Database object * - * @param string $key Hash to validate sending user's password - * @param string $login The user login - * @return object|WP_Error User's database row on success, error object for invalid keys + * @param string $key Hash to validate sending user's password. + * @param string $login The user login. + * @return WP_User|WP_Error WP_User object on success, WP_Error object for invalid or expired keys. */ -function check_password_reset_key( $key, $login ) { - global $wpdb; +function check_password_reset_key($key, $login) { + global $wpdb, $wp_hasher; - $key = preg_replace( '/[^a-z0-9]/i', '', $key ); + $key = preg_replace('/[^a-z0-9]/i', '', $key); - if ( empty( $key ) || ! is_string( $key ) ) - return new WP_Error( 'invalid_key', __( 'Invalid key' ) ); + if ( empty( $key ) || !is_string( $key ) ) + return new WP_Error('invalid_key', __('Invalid key')); - if ( empty( $login ) || ! is_string( $login ) ) - return new WP_Error( 'invalid_key', __( 'Invalid key' ) ); + if ( empty($login) || !is_string($login) ) + return new WP_Error('invalid_key', __('Invalid key')); - $user = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE user_activation_key = %s AND user_login = %s", $key, $login ) ); + $row = $wpdb->get_row( $wpdb->prepare( "SELECT ID, user_activation_key FROM $wpdb->users WHERE user_login = %s", $login ) ); + if ( ! $row ) + return new WP_Error('invalid_key', __('Invalid key')); - if ( empty( $user ) ) - return new WP_Error( 'invalid_key', __( 'Invalid key' ) ); + if ( empty( $wp_hasher ) ) { + require_once ABSPATH . 'wp-includes/class-phpass.php'; + $wp_hasher = new PasswordHash( 8, true ); + } - return $user; + if ( $wp_hasher->CheckPassword( $key, $row->user_activation_key ) ) + return get_userdata( $row->ID ); + + if ( $key === $row->user_activation_key ) { + $return = new WP_Error( 'expired_key', __( 'Invalid key' ) ); + $user_id = $row->ID; + + /** + * Filter the return value of check_password_reset_key() when an + * old-style key is used (plain-text key was stored in the database). + * + * @since 3.7.0 + * + * @param WP_Error $return A WP_Error object denoting an expired key. + * Return a WP_User object to validate the key. + * @param int $user_id The matched user ID. + */ + return apply_filters( 'password_reset_key_expired', $return, $user_id ); + } + + return new WP_Error( 'invalid_key', __( 'Invalid key' ) ); } /** diff --git a/src/wp-login.php b/src/wp-login.php index 6513f777ce..111eddc1cf 100644 --- a/src/wp-login.php +++ b/src/wp-login.php @@ -202,7 +202,7 @@ function wp_login_viewport_meta() { * @return bool|WP_Error True: when finish. WP_Error on error */ function retrieve_password() { - global $wpdb, $current_site; + global $wpdb, $current_site, $wp_hasher; $errors = new WP_Error(); @@ -241,14 +241,27 @@ function retrieve_password() { else if ( is_wp_error($allow) ) return $allow; - $key = $wpdb->get_var($wpdb->prepare("SELECT user_activation_key FROM $wpdb->users WHERE user_login = %s", $user_login)); - if ( empty($key) ) { - // Generate something random for a key... - $key = wp_generate_password(20, false); - do_action('retrieve_password_key', $user_login, $key); - // Now insert the new md5 key into the db - $wpdb->update($wpdb->users, array('user_activation_key' => $key), array('user_login' => $user_login)); + // Generate something random for a password reset key. + $key = wp_generate_password( 20, false ); + + /** + * Fires when a password reset key is generated. + * + * @since 2.5.0 + * + * @param string $user_login The username for the user. + * @param string $key The generated password reset key. + */ + do_action( 'retrieve_password_key', $user_login, $key ); + + // Now insert the key, hashed, into the DB. + if ( empty( $wp_hasher ) ) { + require_once ABSPATH . 'wp-includes/class-phpass.php'; + $wp_hasher = new PasswordHash( 8, true ); } + $hashed = $wp_hasher->HashPassword( $key ); + $wpdb->update( $wpdb->users, array( 'user_activation_key' => $hashed ), array( 'user_login' => $user_login ) ); + $message = __('Someone requested that the password be reset for the following account:') . "\r\n\r\n"; $message .= network_home_url( '/' ) . "\r\n\r\n"; $message .= sprintf(__('Username: %s'), $user_login) . "\r\n\r\n"; @@ -358,7 +371,13 @@ case 'retrievepassword' : } } - if ( isset($_GET['error']) && 'invalidkey' == $_GET['error'] ) $errors->add('invalidkey', __('Sorry, that key does not appear to be valid.')); + if ( isset( $_GET['error'] ) ) { + if ( 'invalidkey' == $_GET['error'] ) + $errors->add( 'invalidkey', __( 'Sorry, that key does not appear to be valid.' ) ); + elseif ( 'expiredkey' == $_GET['error'] ) + $errors->add( 'expiredkey', __( 'Sorry, that key has expired. Please try again.' ) ); + } + $redirect_to = apply_filters( 'lostpassword_redirect', !empty( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : '' ); do_action('lost_password'); @@ -394,7 +413,10 @@ case 'rp' : $user = check_password_reset_key($_GET['key'], $_GET['login']); if ( is_wp_error($user) ) { - wp_redirect( site_url('wp-login.php?action=lostpassword&error=invalidkey') ); + if ( $user->get_error_code() === 'expired_key' ) + wp_redirect( site_url( 'wp-login.php?action=lostpassword&error=expiredkey' ) ); + else + wp_redirect( site_url( 'wp-login.php?action=lostpassword&error=invalidkey' ) ); exit; }