The Exploit
Unauthenticated attackers can abuse the Branda login-screen signup-password flow to change a user password by POSTing password_1 and an activation key.
curl -i -s -X POST 'https://TARGET/wp-login.php?action=activate' \
-d 'key=ANY_VALID_OR_GUESSED_KEY' \
-d 'password_1=P@ssw0rd123!' \
-d 'password_2=P@ssw0rd123!'
The response on a vulnerable site returns the normal activation completion flow, typically a redirect or success message, and the target account identified by key now accepts the supplied password.
If the attacker targets an administrator account, this can immediately lead to full takeover because Branda updates the password directly without verifying the requester’s identity.
What the Patch Did
Before:
public function password_random_password_filter( $password ) {
global $wpdb, $signup_password_use_encryption;
if ( isset( $_GET['key'] ) && ! empty( $_GET['key'] ) ) {
$key = $_GET['key'];
} elseif ( isset( $_POST['key'] ) && ! empty( $_POST['key'] ) ) {
$key = $_POST['key'];
}
// ... use $key and $_POST['password_1'] to override the password
}
After:
public function wpmu_activate_user_set_password( $user_id, $password, $meta ) {
global $wpdb, $signup_password_use_encryption;
if ( ! empty( $meta['password'] ) ) {
return $meta['password'];
}
return $password;
}
The patch removed the code path that derived the password from raw request data and instead uses activation metadata already associated with the validated signup record. This change shifts trust away from $_GET / $_POST and onto WordPress’s activation flow.
Root Cause
This was an improper authentication/validation bug (CWE-287) triggered by unauthenticated request data crossing into password update logic. The Branda module accepted $_POST['password_1'] during signup/activation and used it to set a password without verifying that the request belonged to the intended user. The attacker-controlled fields key and password_1 flowed from the HTTP request into the password-setting code path, allowing arbitrary account takeover when the plugin resolved the wrong identity or failed to enforce activation metadata.
Why It Works
The load-bearing defect was the direct trust placed in request parameters during password setup. In the old code, the handler read $_GET['key'] / $_POST['key'] and $_POST['password_1'] and let that determine the password to store. If that line is removed, the bug vanishes because no unauthenticated POST can directly drive wp_set_password for another user. The engineer added the new activation hook to ensure only the password already stored in validated signup metadata is used; the rest of the patch is plumbing to migrate from the old filter-based hook into a safer activation callback.
Hardening Checklist
- Use WordPress activation metadata rather than raw request parameters when setting passwords during signup/activation.
- Protect all password-changing POST handlers with
wp_verify_nonce()/check_admin_referer()to prevent forged requests. - When updating another user’s password, verify identity with
current_user_can( 'edit_user', $user_id )or equivalent capability checks. - Avoid direct use of
$_POST['password_1']in password storage paths; only accept password values after the request has passed authentication and authorization checks. - Restrict password-reset and activation handlers to validated flows such as
wpmu_activate_userand do not expose arbitrarykey-based request handling without validation.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-14998