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 <csmall@debian.org>
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 @@
 				<label for="key"><?php _e( 'Activation Key:' ); ?></label>
 				<br /><input type="text" name="key" id="key" value="" size="50" autofocus="autofocus" />
 			</p>
+            <p>
+                <label for="key"><?php _e( 'Signup ID:' ) ?></label>
+                <br /><input type="number" name="signup_id" id="signup_id" value="" size="50" />
+            </p>
 			<p class="submit">
 				<input id="submit" type="submit" name="Submit" class="submit" value="<?php esc_attr_e( 'Activate' ); ?>" />
 			</p>
--- 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 <timestamp>:<hashed_key>.
+	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 );
