The Exploit
An unauthenticated attacker with knowledge of a target WordPress user's email address can hijack that account via social login, even if the user has never previously linked a social account to their WordPress profile.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
action=wpdiscuz_social_login&provider=google&email=admin%40target.com&name=Administrator&token=arbitrary_jwt_token_value
The server responds with a Set-Cookie header containing a valid WordPress session token. The attacker is now logged in as the administrator. No email verification, no account linking consent, no challenge required — the plugin trusts that an email address alone is proof of identity.
What the Patch Did
Before
"user_email" => $wordpressUser["email"],
After
"user_email" => $wordpressUser["email_verified"] == 1 ? $wordpressUser["email"] : $wordpressUser["ID"] . "@wordpress.com",
The patch introduces an email verification check using a conditional operator. When a social login attempt arrives, the plugin now verifies that the WordPress user record has email_verified set to 1 before trusting the email returned by the social provider. If the email is unverified, the plugin substitutes a non-routable placeholder email ({user_ID}@wordpress.com) that cannot match any future social login attempt. This breaks the account-matching logic: an attacker cannot hijack an account using an email they do not control unless WordPress itself has already verified that email belongs to that user account.
Root Cause
CWE-640: Weak Password Recovery Mechanism for Forgotten Password (upstream) and CWE-287: Improper Authentication (direct).
The dataflow: A social provider (Google, Facebook, etc.) returns a JWT token containing an email claim. The plugin extracts $wordpressUser["email"] from the WordPress user record and uses it directly as the social login identifier without checking email_verified. The trust boundary violation occurs at the moment the plugin assumes that because a social provider asserts an email address, any WordPress user with that email must be the same person. In reality, WordPress user emails are user-supplied during registration and are unverified by default. An attacker can register a WordPress account with [email protected], then intercept a social login token for [email protected] from their own social provider account, and the plugin will match them to the existing WordPress user — stealing their account.
Why It Works
The load-bearing line is the conditional $wordpressUser["email_verified"] == 1. Removing it restores the bug: any email match triggers account hijacking. The ternary operator's false branch ($wordpressUser["ID"] . "@wordpress.com") is the amplifier — it guarantees that once a social login attempt lands with an unverified email, subsequent attempts cannot re-match the same account, because no legitimate social provider will ever return [email protected] as a claim. The engineer added the conditional to whitelist only emails WordPress itself has verified (typically via confirmation link), shifting the trust anchor from the social provider to the WordPress installation's own email validation ceremony. Without this check, the plugin treated unverified WordPress emails as a valid authentication factor, which is cryptographically unsound — an email address in transit or at rest is bearer credential, not proof of identity.
Hardening Checklist
-
Implement email verification gates before trusting external claims: Use WordPress's built-in
update_user_meta()to set a verified-email flag only after the user clicks a confirmation link sent to that address. Never auto-trust email addresses from third parties without prior WordPress verification. Seewp_new_user_notification()and custom nonce-based confirmation flows. -
Enforce account-linking consent: Before mapping a social login to an existing WordPress user, verify that the user has explicitly linked their social account via an authenticated settings page. Store the social provider ID and the WordPress user ID in a separate, indexed postmeta table to create an immutable audit trail.
-
Use provider-native identifiers as primary keys: Match on the provider's user ID (Google
sub, Facebookid), not email. Email can be reassigned; provider IDs are immutable per account. Validate that the email claim in the JWT matches the provider ID's canonical email before accepting it. -
Validate JWT signatures and check token expiry: Ensure the token is signed by the claimed provider using their public keys, and reject tokens older than 5 minutes. Use a library like
firebase/jwtto avoid custom parsing. Do not accept unsigned or self-signed tokens. -
Log and alert on account takeover attempts: When a social login matches an existing user with an unverified email, log the provider, email, IP, and timestamp. Send the user a security notice. Consider requiring re-authentication before accepting the link.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-9488