From 531abcbdd457f296c42b8e3cb5906c4b095422a4 Mon Sep 17 00:00:00 2001 From: Andrew Ozz Date: Fri, 6 Apr 2018 19:09:53 +0000 Subject: [PATCH] Privacy: fixes and updates for the method to confirm user requests by email. - Improve function and variable names. - Allow extra data to be passed with the request. - Make the option/user meta names more consistent. - Adds an inline comment explaining use of hash. Props mikejolley. See #43443. git-svn-id: https://develop.svn.wordpress.org/trunk@42964 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/user.php | 219 ++++++++++++++++++++++----------------- src/wp-login.php | 8 +- 2 files changed, 130 insertions(+), 97 deletions(-) diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 476aa51424..2c38690252 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -2815,18 +2815,26 @@ function new_user_email_admin_notice() { * * @since 5.0.0 * - * @param string $action_name Name of the action that is being confirmed. - * @param string $action_description User facing description of the action they will be confirming. - * @param string $email User email address. This can be the address of a registered or non-registered user. Defaults to logged in user email address. - * - * @return WP_ERROR|bool Will return true/false based on the success of sending the email, or a WP_Error object. + * @param string $email User email address. This can be the address of a registered or non-registered user. Defaults to logged in user email address. + * @param string $action_name Name of the action that is being confirmed. Defaults to 'confirm_email'. + * @param string $action_description User facing description of the action they will be confirming. Defaults to "confirm your email address". + * @param array $request_data Misc data you want to send with the verification request and pass to the actions once the request is confirmed. + * @return WP_Error|bool Will return true/false based on the success of sending the email, or a WP_Error object. */ -function send_confirm_account_action_email( $action_name, $action_description = '', $email = '' ) { +function wp_send_account_verification_key( $email = '', $action_name = '', $action_description = '', $request_data = array() ) { + if ( ! function_exists( 'wp_get_current_user' ) ) { + return new WP_Error( 'invalid', __( 'This function cannot be used before init.' ) ); + } + $action_name = sanitize_key( $action_name ); $action_description = wp_kses_post( $action_description ); if ( empty( $action_name ) ) { - return new WP_Error( 'invalid_action', __( 'Invalid action' ) ); + $action_name = 'confirm_email'; + } + + if ( empty( $action_description ) ) { + $action_description = __( 'Confirm your email address.' ); } if ( empty( $email ) ) { @@ -2846,30 +2854,31 @@ function send_confirm_account_action_email( $action_name, $action_description = $user = get_user_by( 'email', $email ); } - // We could be dealing with a registered user account, or a visitor. - $is_registered_user = $user && ! is_wp_error( $user ); - $uid = $is_registered_user ? $user->ID : hash( 'sha256', $email ); - $confirm_key = get_confirm_account_action_key( $action_name, $email ); + $confirm_key = wp_get_account_verification_key( $email, $action_name, $request_data ); if ( is_wp_error( $confirm_key ) ) { return $confirm_key; } - // Prepare the email content. - if ( ! $action_description ) { - $action_description = $action_name; + // We could be dealing with a registered user account, or a visitor. + $is_registered_user = $user && ! is_wp_error( $user ); + + if ( $is_registered_user ) { + $uid = $user->ID; + } else { + // Generate a UID for this email address so we don't send the actual email in the query string. Hash is not supported on all systems. + $uid = function_exists( 'hash' ) ? hash( 'sha256', $email ) : sha1( $email ); } /* translators: Do not translate DESCRIPTION, CONFIRM_URL, EMAIL, SITENAME, SITEURL: those are placeholders. */ $email_text = __( 'Howdy, -An account linked to your email address has requested to perform -the following action: +A request has been made to perform the following action on your account: ###DESCRIPTION### -To confirm this action, please click on the following link: +To confirm this, please click on the following link: ###CONFIRM_URL### You can safely ignore and delete this email if you do not want to @@ -2887,7 +2896,7 @@ All at ###SITENAME### 'email' => $email, 'description' => $action_description, 'confirm_url' => add_query_arg( array( - 'action' => 'emailconfirm', + 'action' => 'verifyaccount', 'confirm_action' => $action_name, 'uid' => $uid, 'confirm_key' => $confirm_key, @@ -2907,6 +2916,8 @@ All at ###SITENAME### * ###SITENAME### The name of the site. * ###SITEURL### The URL to the site. * + * @since 5.0.0 + * * @param string $email_text Text in the email. * @param array $email_data { * Data relating to the account action email. @@ -2919,7 +2930,7 @@ All at ###SITENAME### * @type string $siteurl The site URL sending the mail. * } */ - $content = apply_filters( 'confirm_account_action_email_content', $email_text, $email_data ); + $content = apply_filters( 'account_verification_email_content', $email_text, $email_data ); $content = str_replace( '###DESCRIPTION###', $email_data['description'], $content ); $content = str_replace( '###CONFIRM_URL###', esc_url_raw( $email_data['confirm_url'] ), $content ); @@ -2928,7 +2939,7 @@ All at ###SITENAME### $content = str_replace( '###SITEURL###', esc_url_raw( $email_data['siteurl'] ), $content ); /* translators: %s Site name. */ - return wp_mail( $email_data['email'], sprintf( __( '[%s] Confirm Account Action' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ), $content ); + return wp_mail( $email_data['email'], sprintf( __( '[%s] Confirm Action' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ), $content ); } /** @@ -2936,12 +2947,12 @@ All at ###SITENAME### * * @since 5.0.0 * - * @param string $action_name Name of the action this key is being generated for. - * @param string $email User email address. This can be the address of a registered or non-registered user. - * + * @param string $email User email address. This can be the address of a registered or non-registered user. + * @param string $action_name Name of the action this key is being generated for. + * @param array $request_data Misc data you want to send with the verification request and pass to the actions once the request is confirmed. * @return string|WP_Error Confirmation key on success. WP_Error on error. */ -function get_confirm_account_action_key( $action_name, $email ) { +function wp_get_account_verification_key( $email, $action_name, $request_data = array() ) { global $wp_hasher; if ( ! is_email( $email ) ) { @@ -2967,15 +2978,23 @@ function get_confirm_account_action_key( $action_name, $email ) { } $hashed_key = $wp_hasher->HashPassword( $key ); + $value = array( + 'action' => $action_name, + 'time' => time(), + 'hash' => $hashed_key, + 'email' => $email, + 'request_data' => $request_data, + ); if ( $is_registered_user ) { - $key_saved = (bool) update_user_meta( $user->ID, '_account_action_' . $action_name, implode( ':', array( time(), $hashed_key ) ) ); + $key_saved = (bool) update_user_meta( $user->ID, '_verify_action_' . $action_name, wp_json_encode( $value ) ); } else { - $key_saved = (bool) update_site_option( '_account_action_' . hash( 'sha256', $email ) . '_' . $action_name, implode( ':', array( time(), $hashed_key, $email ) ) ); + $uid = function_exists( 'hash' ) ? hash( 'sha256', $email ) : sha1( $email ); + $key_saved = (bool) update_site_option( '_verify_action_' . $action_name . '_' . $uid, wp_json_encode( $value ) ); } if ( false === $key_saved ) { - return new WP_Error( 'no_confirm_account_action_key_update', __( 'Could not save confirm account action key to database.' ) ); + return new WP_Error( 'no_account_verification_key_update', __( 'Could not save confirm account action key to database.' ) ); } return $key; @@ -2986,81 +3005,95 @@ function get_confirm_account_action_key( $action_name, $email ) { * * @since 5.0.0 * - * @param string $action_name Name of the action this key is being generated for. * @param string $key Key to confirm. * @param string $uid Email hash or user ID. - * + * @param string $action_name Name of the action this key is being generated for. * @return array|WP_Error WP_Error on failure, action name and user email address on success. */ -function check_confirm_account_action_key( $action_name, $key, $uid ) { +function wp_check_account_verification_key( $key, $uid, $action_name ) { global $wp_hasher; - if ( ! empty( $action_name ) && ! empty( $key ) && ! empty( $uid ) ) { - $user = false; + if ( empty( $action_name ) || empty( $key ) || empty( $uid ) ) { + return new WP_Error( 'invalid_key', __( 'Invalid key' ) ); + } - if ( is_numeric( $uid ) ) { - $user = get_user_by( 'id', absint( $uid ) ); + $user = false; + + if ( is_numeric( $uid ) ) { + $user = get_user_by( 'id', absint( $uid ) ); + } + + // We could be dealing with a registered user account, or a visitor. + $is_registered_user = ( $user && ! is_wp_error( $user ) ); + $key_request_time = ''; + $saved_key = ''; + $email = ''; + + if ( empty( $wp_hasher ) ) { + require_once ABSPATH . WPINC . '/class-phpass.php'; + $wp_hasher = new PasswordHash( 8, true ); + } + + // Get the saved key from the database. + if ( $is_registered_user ) { + $raw_data = get_user_meta( $user->ID, '_verify_action_' . $action_name, true ); + $email = $user->user_email; + + if ( false !== strpos( $confirm_action_data, ':' ) ) { + list( $key_request_time, $saved_key ) = explode( ':', $confirm_action_data, 2 ); } + } else { + $raw_data = get_site_option( '_verify_action_' . $action_name . '_' . $uid, '' ); - // We could be dealing with a registered user account, or a visitor. - $is_registered_user = $user && ! is_wp_error( $user ); - $key_request_time = ''; - $saved_key = ''; - $email = ''; - - if ( empty( $wp_hasher ) ) { - require_once ABSPATH . WPINC . '/class-phpass.php'; - $wp_hasher = new PasswordHash( 8, true ); - } - - // Get the saved key from the database. - if ( $is_registered_user ) { - $confirm_action_data = get_user_meta( $user->ID, '_account_action_' . $action_name, true ); - $email = $user->user_email; - - if ( false !== strpos( $confirm_action_data, ':' ) ) { - list( $key_request_time, $saved_key ) = explode( ':', $confirm_action_data, 2 ); - } - } else { - $confirm_action_data = get_site_option( '_account_action_' . $uid . '_' . $action_name, '' ); - - if ( false !== strpos( $confirm_action_data, ':' ) ) { - list( $key_request_time, $saved_key, $email ) = explode( ':', $confirm_action_data, 3 ); - } - } - - if ( ! $saved_key ) { - return new WP_Error( 'invalid_key', __( 'Invalid key' ) ); - } - - /** - * Filters the expiration time of confirm keys. - * - * @param int $expiration The expiration time in seconds. - */ - $expiration_duration = apply_filters( 'account_action_expiration', DAY_IN_SECONDS ); - $expiration_time = $key_request_time + $expiration_duration; - - if ( $wp_hasher->CheckPassword( $key, $saved_key ) ) { - if ( $expiration_time && time() < $expiration_time ) { - $return = array( - 'action' => $action_name, - 'email' => $email, - ); - } else { - $return = new WP_Error( 'expired_key', __( 'The confirmation email has expired.' ) ); - } - - // Clean up stored keys. - if ( $is_registered_user ) { - delete_user_meta( $user->ID, '_account_action_' . $action_name ); - } else { - delete_site_option( '_account_action_' . $uid . '_' . $action_name ); - } - - return $return; + if ( false !== strpos( $confirm_action_data, ':' ) ) { + list( $key_request_time, $saved_key, $email ) = explode( ':', $confirm_action_data, 3 ); } } - return new WP_Error( 'invalid_key', __( 'Invalid key' ) ); -} + $data = json_decode( $raw_data, true ); + $key_request_time = (int) isset( $data['time'] ) ? $data['time'] : 0; + $saved_key = isset( $data['hash'] ) ? $data['hash'] : ''; + $email = sanitize_email( isset( $data['email'] ) ? $data['email'] : '' ); + $request_data = isset( $data['request_data'] ) ? $data['request_data'] : array(); + + if ( ! $saved_key ) { + return new WP_Error( 'invalid_key', __( 'Invalid key' ) ); + } + + if ( ! $key_request_time || ! $email ) { + return new WP_Error( 'invalid_key', __( 'Invalid action' ) ); + } + + /** + * Filters the expiration time of confirm keys. + * + * @since 5.0.0 + * + * @param int $expiration The expiration time in seconds. + */ + $expiration_duration = apply_filters( 'account_verification_expiration', DAY_IN_SECONDS ); + $expiration_time = $key_request_time + $expiration_duration; + + if ( ! $wp_hasher->CheckPassword( $key, $saved_key ) ) { + return new WP_Error( 'invalid_key', __( 'Invalid key' ) ); + } + + if ( $expiration_time && time() < $expiration_time ) { + $return = array( + 'action' => $action_name, + 'email' => $email, + 'request_data' => $request_data, + ); + } else { + $return = new WP_Error( 'expired_key', __( 'The confirmation email has expired.' ) ); + } + + // Clean up stored keys. + if ( $is_registered_user ) { + delete_user_meta( $user->ID, '_verify_action_' . $action_name ); + } else { + delete_site_option( '_verify_action_' . $action_name . '_' . $uid ); + } + + return $return; +} \ No newline at end of file diff --git a/src/wp-login.php b/src/wp-login.php index e7467cb5b8..0e9ba0ac96 100644 --- a/src/wp-login.php +++ b/src/wp-login.php @@ -427,7 +427,7 @@ if ( isset( $_GET['key'] ) ) { } // validate action so as to default to the login screen -if ( ! in_array( $action, array( 'postpass', 'logout', 'lostpassword', 'retrievepassword', 'resetpass', 'rp', 'register', 'login', 'emailconfirm' ), true ) && false === has_filter( 'login_form_' . $action ) ) { +if ( ! in_array( $action, array( 'postpass', 'logout', 'lostpassword', 'retrievepassword', 'resetpass', 'rp', 'register', 'login', 'verifyaccount' ), true ) && false === has_filter( 'login_form_' . $action ) ) { $action = 'login'; } @@ -858,12 +858,12 @@ switch ( $action ) { break; - case 'emailconfirm' : + case 'verifyaccount' : if ( isset( $_GET['confirm_action'], $_GET['confirm_key'], $_GET['uid'] ) ) { - $action_name = sanitize_key( wp_unslash( $_GET['confirm_action'] ) ); $key = sanitize_text_field( wp_unslash( $_GET['confirm_key'] ) ); $uid = sanitize_text_field( wp_unslash( $_GET['uid'] ) ); - $result = check_confirm_account_action_key( $action_name, $key, $uid ); + $action_name = sanitize_key( wp_unslash( $_GET['confirm_action'] ) ); + $result = wp_check_account_verification_key( $key, $uid, $action_name ); } else { $result = new WP_Error( 'invalid_key', __( 'Invalid key' ) ); }