Description: Use hash for user activation key Removes cleartext of the user activation key CVE-2017-14990 Author: timdxw Origin: upstream, https://core.trac.wordpress.org/ticket/38474 Bug-Debian: https://bugs.debian.org/877629 Reviewed-by: Craig Small Last-Update: 2023-08-10 --- a/wp-activate.php +++ b/wp-activate.php @@ -42,13 +42,15 @@ wp_safe_redirect( $redirect_url ); exit; } else { - $result = wpmu_activate_signup( $key ); + $signup_id = ! empty( $_GET['signup_id'] ) ? $_GET['signup_id'] : $_POST['signup_id']; + $result = wpmu_activate_signup( $key, $signup_id ); } } if ( null === $result && isset( $_COOKIE[ $activate_cookie ] ) ) { $key = $_COOKIE[ $activate_cookie ]; - $result = wpmu_activate_signup( $key ); + $signup_id = ! empty( $_GET['signup_id'] ) ? $_GET['signup_id'] : $_POST['signup_id']; + $result = wpmu_activate_signup( $key, $signup_id ); setcookie( $activate_cookie, ' ', time() - YEAR_IN_SECONDS, $activate_path, COOKIE_DOMAIN, is_ssl(), true ); } @@ -133,6 +135,10 @@

+

+ +
+

--- a/wp-admin/user-new.php +++ b/wp-admin/user-new.php @@ -227,8 +227,8 @@ ) ); if ( isset( $_POST['noconfirmation'] ) && current_user_can( 'manage_network_users' ) ) { - $key = $wpdb->get_var( $wpdb->prepare( "SELECT activation_key FROM {$wpdb->signups} WHERE user_login = %s AND user_email = %s", $new_user_login, $new_user_email ) ); - $new_user = wpmu_activate_signup( $key ); + $row = $wpdb->get_row( $wpdb->prepare( "SELECT activation_key, signup_id FROM {$wpdb->signups} WHERE user_login = %s AND user_email = %s", $new_user_login, $new_user_email ) ); + $new_user = wpmu_activate_signup( $row['activation_key'], $row['signup_id'] ); if ( is_wp_error( $new_user ) ) { $redirect = add_query_arg( array( 'update' => 'addnoconfirmation' ), 'user-new.php' ); } elseif ( ! is_user_member_of_blog( $new_user['user_id'] ) ) { --- a/wp-includes/ms-default-filters.php +++ b/wp-includes/ms-default-filters.php @@ -26,7 +26,7 @@ add_action( 'wpmu_new_user', 'newuser_notify_siteadmin' ); add_action( 'wpmu_activate_user', 'add_new_user_to_blog', 10, 3 ); add_action( 'wpmu_activate_user', 'wpmu_welcome_user_notification', 10, 3 ); -add_action( 'after_signup_user', 'wpmu_signup_user_notification', 10, 4 ); +add_action( 'after_signup_user', 'wpmu_signup_user_notification', 10, 5 ); add_action( 'network_site_new_created_user', 'wp_send_new_user_notifications' ); add_action( 'network_site_users_created_user', 'wp_send_new_user_notifications' ); add_action( 'network_user_new_created_user', 'wp_send_new_user_notifications' ); @@ -39,7 +39,7 @@ // Blogs. add_filter( 'wpmu_validate_blog_signup', 'signup_nonce_check' ); add_action( 'wpmu_activate_blog', 'wpmu_welcome_notification', 10, 5 ); -add_action( 'after_signup_site', 'wpmu_signup_blog_notification', 10, 7 ); +add_action( 'after_signup_site', 'wpmu_signup_blog_notification', 10, 8 ); add_filter( 'wp_normalize_site_data', 'wp_normalize_site_data', 10, 1 ); add_action( 'wp_validate_site_data', 'wp_validate_site_data', 10, 3 ); add_action( 'wp_insert_site', 'wp_maybe_update_network_site_counts_on_update', 10, 1 ); --- a/wp-includes/ms-functions.php +++ b/wp-includes/ms-functions.php @@ -783,10 +783,17 @@ * @param array $meta Optional. Signup meta data. By default, contains the requested privacy setting and lang_id. */ function wpmu_signup_blog( $domain, $path, $title, $user, $user_email, $meta = array() ) { - global $wpdb; + global $wpdb, $wp_hasher; $key = substr( md5( time() . wp_rand() . $domain ), 0, 16 ); + if ( empty( $wp_hasher ) ) { + require_once ABSPATH . WPINC . '/class-phpass.php'; + $wp_hasher = new PasswordHash( 8, true ); + } + + $hashed = time() . ':' . $wp_hasher->HashPassword( $key ); + /** * Filters the metadata for a site signup. * @@ -801,8 +808,9 @@ * @param string $user The user's requested login name. * @param string $user_email The user's email address. * @param string $key The user's activation key. + * @param string $hashed The user's hashed activation key. */ - $meta = apply_filters( 'signup_site_meta', $meta, $domain, $path, $title, $user, $user_email, $key ); + $meta = apply_filters( 'signup_site_meta', $meta, $domain, $path, $title, $user, $user_email, $key, $hashed ); $wpdb->insert( $wpdb->signups, @@ -813,7 +821,7 @@ 'user_login' => $user, 'user_email' => $user_email, 'registered' => current_time( 'mysql', true ), - 'activation_key' => $key, + 'activation_key' => $hashed, 'meta' => serialize( $meta ), ) ); @@ -830,8 +838,10 @@ * @param string $user_email The user's email address. * @param string $key The user's activation key. * @param array $meta Signup meta data. By default, contains the requested privacy setting and lang_id. + * @param int $signup_id Signup ID. + * @param string $hashed The user's hashed activation key. */ - do_action( 'after_signup_site', $domain, $path, $title, $user, $user_email, $key, $meta ); + do_action( 'after_signup_site', $domain, $path, $title, $user, $user_email, $key, $meta, $wpdb->insert_id, $hashed ); } /** @@ -849,13 +859,20 @@ * @param array $meta Optional. Signup meta data. Default empty array. */ function wpmu_signup_user( $user, $user_email, $meta = array() ) { - global $wpdb; + global $wpdb, $wp_hasher; // Format data. $user = preg_replace( '/\s+/', '', sanitize_user( $user, true ) ); $user_email = sanitize_email( $user_email ); $key = substr( md5( time() . wp_rand() . $user_email ), 0, 16 ); + if ( empty( $wp_hasher ) ) { + require_once ABSPATH . WPINC . '/class-phpass.php'; + $wp_hasher = new PasswordHash( 8, true ); + } + + $hashed = time() . ':' . $wp_hasher->HashPassword( $key ); + /** * Filters the metadata for a user signup. * @@ -867,8 +884,9 @@ * @param string $user The user's requested login name. * @param string $user_email The user's email address. * @param string $key The user's activation key. + * @param string $hashed The user's hashed activation key. */ - $meta = apply_filters( 'signup_user_meta', $meta, $user, $user_email, $key ); + $meta = apply_filters( 'signup_user_meta', $meta, $user, $user_email, $key, $hashed ); $wpdb->insert( $wpdb->signups, @@ -879,7 +897,7 @@ 'user_login' => $user, 'user_email' => $user_email, 'registered' => current_time( 'mysql', true ), - 'activation_key' => $key, + 'activation_key' => $hashed, 'meta' => serialize( $meta ), ) ); @@ -893,8 +911,10 @@ * @param string $user_email The user's email address. * @param string $key The user's activation key. * @param array $meta Signup meta data. Default empty array. + * @param int $signup_id Signup ID. + * @param string $hashed The user's hashed activation key. */ - do_action( 'after_signup_user', $user, $user_email, $key, $meta ); + do_action( 'after_signup_user', $user, $user_email, $key, $meta, $wpdb->insert_id, $hashed ); } /** @@ -920,9 +940,10 @@ * @param string $user_email The user's email address. * @param string $key The activation key created in wpmu_signup_blog(). * @param array $meta Optional. Signup meta data. By default, contains the requested privacy setting and lang_id. + * @param int $signup_id Signup ID. * @return bool */ -function wpmu_signup_blog_notification( $domain, $path, $title, $user_login, $user_email, $key, $meta = array() ) { +function wpmu_signup_blog_notification( $domain, $path, $title, $user_login, $user_email, $key, $meta = array(), $signup_id ) { /** * Filters whether to bypass the new site email notification. * @@ -942,9 +963,9 @@ // Send email with activation link. if ( ! is_subdomain_install() || get_current_network_id() != 1 ) { - $activate_url = network_site_url( "wp-activate.php?key=$key" ); + $activate_url = network_site_url( "wp-activate.php?key=$key&signup_id=$signup_id" ); } else { - $activate_url = "http://{$domain}{$path}wp-activate.php?key=$key"; // @todo Use *_url() API. + $activate_url = "http://{$domain}{$path}wp-activate.php?key=$key&signup_id=$signup_id"; // @todo use *_url() API } $activate_url = esc_url( $activate_url ); @@ -1055,9 +1076,10 @@ * @param string $user_email The user's email address. * @param string $key The activation key created in wpmu_signup_user() * @param array $meta Optional. Signup meta data. Default empty array. + * @param int $signup_id Signup ID. * @return bool */ -function wpmu_signup_user_notification( $user_login, $user_email, $key, $meta = array() ) { +function wpmu_signup_user_notification( $user_login, $user_email, $key, $meta = array(), $signup_id ) { /** * Filters whether to bypass the email notification for new user sign-up. * @@ -1107,7 +1129,7 @@ $key, $meta ), - site_url( "wp-activate.php?key=$key" ) + site_url( "wp-activate.php?key=$key&signup_id=$signup_id" ) ); $subject = sprintf( @@ -1157,17 +1179,53 @@ * @global wpdb $wpdb WordPress database abstraction object. * * @param string $key The activation key provided to the user. + * @param int $signup_id The Signup ID. * @return array|WP_Error An array containing information about the activated user and/or blog. */ -function wpmu_activate_signup( $key ) { - global $wpdb; +function wpmu_activate_signup( $key, $signup_id ) { + global $wpdb, $wp_hasher; - $signup = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->signups WHERE activation_key = %s", $key ) ); + $signup = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->signups WHERE activation_key = %s OR signup_id = %d", $key, $signup_id ) ); if ( empty( $signup ) ) { return new WP_Error( 'invalid_key', __( 'Invalid activation key.' ) ); } + // If the key requested matches the actual key in the database, it's a legacy one. + if ( $key === $signup->activation_key ) { + return new WP_Error( 'expired_key', __( 'Invalid key' ) ); + } + + // The format of the new keys is :. + if ( false === strpos( $signup->activation_key, ':' ) ) { + return new WP_Error( 'invalid_key', __( 'Invalid activation key.' ) ); + } + + if ( empty( $wp_hasher ) ) { + require_once ABSPATH . WPINC . '/class-phpass.php'; + $wp_hasher = new PasswordHash( 8, true ); + } + + list( $pass_request_time, $signup_key ) = explode( ':', $signup->activation_key, 2 ); + + if ( ! $wp_hasher->CheckPassword( $key, $signup_key ) ) { + return new WP_Error( 'invalid_key', __( 'Invalid activation key.' ) ); + } + + /** + * Filters the expiration time of signup activation keys. + * + * @since 5.0 + * + * @param int $expiration_duration The expiration time in seconds. + */ + $expiration_duration = apply_filters( 'activate_signup_expiration', DAY_IN_SECONDS ); + $expiration_time = $pass_request_time + $expiration_duration; + + if ( time() > $expiration_time ) { + return new WP_Error( 'expired_key', __( 'Invalid key' ) ); + } + if ( $signup->active ) { if ( empty( $signup->domain ) ) { return new WP_Error( 'already_active', __( 'The user is already active.' ), $signup );