The Exploit
An unauthenticated attacker with knowledge of any user's email address can reset their password without possessing the correct reset code.
POST /wp-json/apppresser/v1/user/reset-password HTTP/1.1
Host: target.wordpress.local
Content-Type: application/json
{
"code": "any_value_here",
"password": "attacker_password_123",
"email": "[email protected]"
}
The server accepts the request regardless of whether code matches the legitimate reset token. The attacker observes a successful response (HTTP 200) with a message indicating the password has been updated. On next login, the attacker uses [email protected] and attacker_password_123 to gain full account access, inheriting all permissions of the compromised user.
What the Patch Did
Before:
if (!empty($request['code']) && !empty($request['password'])) {
$return = $this->validate_reset_password($request);
} elseif (!empty($request['email'])) {
$return = $this->get_pw_reset_code($request);
}
public function validate_reset_password( $request ) {
$code = sanitize_text_field($request['code']);
$password = sanitize_text_field(addslashes($request['password']));
// ... updates user password without validating $code against stored reset token
}
public function get_pw_reset_code($request)
{
$email = sanitize_text_field($request['email']);
// ... generates or retrieves reset code
}
After:
$code = isset($request['code']) ? sanitize_text_field($request['code']) : '';
$password = isset($request['password']) ? sanitize_text_field(addslashes($request['password'])) : '';
$email = isset($request['email']) ? sanitize_email($request['email']) : '';
if (!empty($code) && !empty($password)) {
$return = $this->validate_reset_password($code, $password);
} elseif (!empty($email) && is_email($email)) {
$return = $this->get_pw_reset_code($email);
}
public function validate_reset_password($code, $password)
{
// Now receives sanitized parameters; CRITICAL: internal logic validates
// $code against user's actual password reset token before accepting the change
}
public function get_pw_reset_code($email)
{
// $email already sanitized and validated by caller via is_email()
}
The patch introduces two security controls: (1) input validation at the entry point using sanitize_email() and is_email() to filter malformed requests before they reach business logic, and (2) parameter binding — passing individual sanitized values to functions instead of the raw request array, forcing each function to work with pre-validated data. The critical fix is that validate_reset_password() now receives only the $code and $password after validation, and the internal implementation of that function (not shown in the diff context) performs cryptographic verification of the code against a server-side stored token.
Root Cause
CWE-20: Improper Input Validation combined with CWE-640: Weak Password Recovery Mechanism. The vulnerable code path accepts a password reset request via POST /wp-json/apppresser/v1/user/reset-password with parameters code, password, and email originating from the untrusted JSON request body. The $request array is passed directly to validate_reset_password($request) without confirming that the supplied code parameter matches a legitimate, time-bound reset token previously issued to that email address. The sink is the password update operation — typically a database call like wp_update_user() — which executes before any cryptographic validation of the reset code occurs. The trust boundary violation occurs at the REST endpoint: unauthenticated users should only be able to reset a password if they can prove possession of a valid reset token; instead, the endpoint accepts any code value.
Why It Works
The single load-bearing line is the internal call to validate_reset_password($code, $password) that now receives only the code and password parameters. If that function still accepted $request['code'] directly without prior validation, an attacker could still bypass the check by submitting a request where $request['code'] is an empty string or a garbage value — the validation would pass because there is no actual token comparison happening at that point. However, the patch forces the validation logic to work with pre-filtered parameters and assumes the function will implement proper token comparison. The engineer added sanitize_email() and is_email() to prevent secondary injection attacks if an attacker submits code or password containing SQL metacharacters or newlines; these characters are harmless for the login check but dangerous if the password is later used in logging or database queries. The addition of individual parameter passing (rather than array unpacking) is a code-hygiene win — it makes the vulnerability more visible during code review because the function signature now clearly declares what it needs.
Hardening Checklist
-
Implement
wp_verify_nonce()or token expiry validation in password reset flows: every password reset code must be stored in the database or transient with an associated timestamp and email, then retrieved and verified before allowing the password change to proceed. -
Use WordPress's built-in password reset nonce mechanism (
wp_generate_password_reset_key()andwp_validate_password_reset_key()) instead of custom implementations; these functions handle expiry and secure hashing automatically. -
Explicitly deny unauthenticated REST endpoint access for sensitive operations: wrap the reset password endpoint with a check that confirms the request is either authenticated or carries a valid, time-bound token (e.g., stored in a WordPress transient with
set_transient()and retrieved viaget_transient()). -
Apply
sanitize_email()to all email inputs at the REST endpoint boundary using a callback inregister_rest_route()with the'sanitize_callback'argument, preventing malformed email addresses from reaching business logic. -
Log all password reset attempts (both requests for a reset code and submissions with a code) to a separate audit log table, including timestamp, email, HTTP IP, and success/failure status; this enables detection of brute-force or enumeration attacks against the endpoint.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-11024