The Exploit
An unauthenticated attacker who knows an arbitrary WordPress user's meta key and can predict or enumerate meta values can bypass authentication entirely and log in as that user. No nonce, no password, no session required.
GET /wp-login.php?mls_temp_login_token=attacker_controlled_value HTTP/1.1
Host: target.wordpress.local
User-Agent: Mozilla/5.0
Cookie: wordpress_logged_in=; wp-settings-time=
When the token parameter matches a user meta value stored in the database (e.g., a temporary login token previously created by an admin), the get_valid_user_based_on_token() function retrieves that user's ID and logs them in via wp_set_current_user() and wp_set_auth_cookie() without any capability checks. An attacker observes a successful 302 redirect to /wp-admin/ with an authenticated session cookie. If the target user is an administrator, the attacker gains full site control.
What the Patch Did
Before:
public static function get_valid_user_based_on_token( $token ) {
if ( empty( $token ) ) {
return false;
}
global $wpdb;
$meta_key = 'mls_temp_user_expires_on';
$sql = '
SELECT ID, display_name
FROM ' . $wpdb->users . ' INNER JOIN ' . $wpdb->usermeta . '
ON ' . $wpdb->users . '.ID = ' . $wpdb->usermeta . '.user_id
WHERE ' . $wpdb->usermeta . '.meta_key = "mls_temp_user_token"
AND ' . $wpdb->usermeta . '.meta_value = %s
AND ' . $wpdb->usermeta . '.user_id NOT IN (
SELECT user_id FROM ' . $wpdb->usermeta . ' WHERE meta_key = "mls_temp_user_expires_on" AND CAST( meta_value AS UNSIGNED ) < %d
)
';
$result = $wpdb->get_row( $wpdb->prepare( $sql, $token, time() ) );
if ( ! empty( $result ) && ! empty( $result->ID ) ) {
wp_set_current_user( $result->ID );
wp_set_auth_cookie( $result->ID );
return $result;
}
return false;
}
After:
public static function get_valid_user_based_on_token( $token ) {
if ( empty( $token ) ) {
return false;
}
if ( ! \is_user_logged_in() ) {
return false;
}
global $wpdb;
$meta_key = 'mls_temp_user_expires_on';
$sql = '
SELECT ID, display_name
FROM ' . $wpdb->users . ' INNER JOIN ' . $wpdb->usermeta . '
ON ' . $wpdb->users . '.ID = ' . $wpdb->usermeta . '.user_id
WHERE ' . $wpdb->usermeta . '.meta_key = "mls_temp_user_token"
AND ' . $wpdb->usermeta . '.meta_value = %s
AND ' . $wpdb->usermeta . '.user_id NOT IN (
SELECT user_id FROM ' . $wpdb->usermeta . '.meta_key = "mls_temp_user_expires_on" AND CAST( meta_value AS UNSIGNED ) < %d
)
';
$result = $wpdb->get_row( $wpdb->prepare( $sql, $token, time() ) );
if ( ! empty( $result ) && ! empty( $result->ID ) ) {
wp_set_current_user( $result->ID );
wp_set_auth_cookie( $result->ID );
return $result;
}
return false;
}
The patch adds a single authorization gate: if ( ! \is_user_logged_in() ) { return false; } at the start of get_valid_user_based_on_token(). This is the WordPress is_user_logged_in() function, which checks whether the current request carries a valid, pre-existing authentication session. The function now refuses to process temporary login tokens unless the caller is already authenticated.
Root Cause
CWE-862: Missing Authorization combined with CWE-287: Improper Authentication.
The vulnerable code path flows as follows: when a user lands on the login page with the mls_temp_login_token parameter set, a hooked handler (not shown in the diff but referenced by the function signature) calls get_valid_user_based_on_token() with the attacker-controlled token from $_REQUEST['mls_temp_login_token']. That parameter enters as untrusted user input. The function performs a SQL query using $wpdb->prepare() to escape it, preventing injection, but does not verify that the requester is already logged in. It then directly calls wp_set_auth_cookie() on the queried user, elevating an anonymous request to authenticated. The trust boundary crossed is: unauthenticated requester → authenticated session, with no authorization check in between.
Why It Works
The load-bearing line is if ( ! \is_user_logged_in() ) { return false; }.
Without this check, an attacker can call the function from unauthenticated context, query the temporary-login meta table for any token value they know or guess, and immediately authenticate as that user. Removing this single line restores the vulnerability.
The surrounding code—the SQL prepare, the expiration check, the user existence validation—is defence in depth: it prevents invalid tokens from triggering a login, and it stops expired temporary-login sessions from being reused. But none of it enforces the critical rule: "only already-authenticated users may perform token-based re-authentication." That rule lives in the one line the patch adds. The other lines were already there; the engineer correctly identified that the missing piece was the authorization gate itself, not the input handling.
Hardening Checklist
-
Add
is_user_logged_in()checks before re-authentication flows. Any function that callswp_set_auth_cookie()orwp_set_current_user()must first verify the requester is already authenticated viais_user_logged_in(), unless the function is explicitly designed for first-time login (e.g.,wp_authenticate()in core). -
Audit all query handlers hooked to
initorwp_loaded. If a handler processes$_REQUESTor$_GETparameters and performs privilege-level changes, enumerate every code path and confirm each one has a capability check (current_user_can()) or authentication check (is_user_logged_in()) at the entry point. -
Use
wp_verify_nonce()for all state-changing operations. The temporary-login feature should require a nonce even for authenticated users to prevent CSRF. Verify the nonce before accepting themls_temp_login_tokenparameter, not after querying the database. -
Test unauthenticated access to every authentication-related endpoint. In your test suite, confirm that calling your token-exchange function with a valid token but no prior session returns
falseand does not callwp_set_auth_cookie().
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-6895