Index: src/wp-includes/user.php
===================================================================
--- src/wp-includes/user.php	(revision 32517)
+++ src/wp-includes/user.php	(working copy)
@@ -2253,6 +2253,13 @@
 
 	$key = preg_replace('/[^a-z0-9]/i', '', $key);
 
+	/**
+	 * Get usermeta that stores a timestamp we check against to make sure
+	 * the reset request isn't too old. 
+	 */
+	$user = get_user_by( 'login', $login );
+	$reset_password_timestamp = get_user_meta( $user->ID, 'reset_password_timestamp', true );
+
 	if ( empty( $key ) || !is_string( $key ) )
 		return new WP_Error('invalid_key', __('Invalid key'));
 
@@ -2268,8 +2275,26 @@
 		$wp_hasher = new PasswordHash( 8, true );
 	}
 
-	if ( $wp_hasher->CheckPassword( $key, $row->user_activation_key ) )
+	if ( empty( $reset_password_timestamp ) ) {
+		return new WP_Error( 'expired_key', __( 'Invalid key' ) );
+	}
+
+	/**
+	 * Filter the password reset expiry duration in seconds.
+	 *
+	 * @since ???
+	 *
+	 * @param int  An integer representing the time in seconds for which a
+	 *             password reset key should be considered valid.
+	 */
+	$reset_password_expiry_in_seconds = apply_filters( 'reset_password_expiry_in_seconds', 4 * HOUR_IN_SECONDS );
+	if ( time() - (int) $reset_password_timestamp > $reset_password_expiry_in_seconds ) {
+		return new WP_Error( 'expired_key', 'Invalid key' );
+	}
+
+	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' ) );
Index: src/wp-login.php
===================================================================
--- src/wp-login.php	(revision 32517)
+++ src/wp-login.php	(working copy)
@@ -403,6 +403,10 @@
 	if ( $message && !wp_mail( $user_email, wp_specialchars_decode( $title ), $message ) )
 		wp_die( __('The e-mail could not be sent.') . "<br />\n" . __('Possible reason: your host may have disabled the mail() function.') );
 
+	// Store a timestamp so that we can expire password resets.
+	$user = get_user_by( 'login', $user_login );
+	update_user_meta( $user->ID, 'reset_password_timestamp', time() );
+
 	return true;
 }
 
@@ -818,6 +822,9 @@
 	$redirect_to = apply_filters( 'login_redirect', $redirect_to, $requested_redirect_to, $user );
 
 	if ( !is_wp_error($user) && !$reauth ) {
+		// Clean up user meta that may have been saved in the case of a password reset request.
+		delete_user_meta( $user->ID, 'reset_password_timestamp' );
+
 		if ( $interim_login ) {
 			$message = '<p class="message">' . __('You have logged in successfully.') . '</p>';
 			$interim_login = 'success';
