The Exploit
An unauthenticated attacker with the ability to intercept or influence the social login callback can log in as any existing WordPress user — including administrator accounts — by submitting a token from any social provider that does not verify the email address associated with the returned profile object.
POST /wp-login.php?action=social_login HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
provider=google&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&[email protected]
The attacker observes a 302 redirect to /wp-admin/ and receives a valid WordPress session cookie (wordpress_logged_in_*). The wp_current_user() global resolves to the administrator account, even though the attacker never possessed valid credentials for that user.
What the Patch Did
Before
if(is_object($getProfile) && !empty($getProfile)) {
$user_email = $getProfile->email;
$emailVerified = !empty($getProfile->emailVerified);
if ($emailVerified || empty($user_email)) {
$setting_data = get_option(\WP\Social\Keys::OK_GLOBAL_SETTINGS);
// Proceeds directly to user creation/login without email verification check
After
$user_email = isset($getProfile->email) ? $getProfile->email : '';
$emailVerified = !empty($getProfile->emailVerified);
if ($emailVerified || empty($user_email)) {
$setting_data = get_option(\WP\Social\Keys::OK_GLOBAL_SETTINGS);
// User creation/login logic wrapped in email verification condition
} else {
die('Email address is not verified or not provided by the social provider.');
}
The patch adds a mandatory check of the $emailVerified flag before user creation or session establishment. When a social provider returns a profile object without marking emailVerified as truthy, the login flow terminates with a fatal error. The secondary fix — wrapping $getProfile->email in isset() — prevents undefined property notices, but is defensive only and not load-bearing for this vulnerability.
Root Cause
CWE-287: Improper Authentication — The plugin fails to validate that the email address returned by the social provider has been verified by that provider. When a user initiates social login, the plugin requests a profile object from the provider (via OAuth token). The plugin extracts $getProfile->email and uses it directly to look up or create a WordPress user without confirming that the provider has verified ownership of that email. An attacker who can influence the social provider's response — or who exploits a provider that returns unverified emails — can specify any existing email address (including [email protected]) and bypass authentication entirely. The trust boundary between "email returned by social provider" and "email verified by social provider" is crossed unchecked.
Why It Works
The load-bearing line is the emailVerified check itself:
if ($emailVerified || empty($user_email)) {
// Proceed with user creation/login
} else {
die('Email address is not verified or not provided by the social provider.');
}
If you removed this condition and returned to the original code, the plugin would accept any email from any social provider response, regardless of whether that provider had validated it. The isset() wrapper around $getProfile->email is a hygiene fix — it prevents PHP notices — but it does not stop the attack. The engineer added the else block with the fatal die() to make the rejection explicit and observable, rather than relying on the conditional logic alone. This is defense-in-depth: the email verification flag is the primary control; the explicit error message is the secondary control that prevents silent failures or bypass attempts that skip the check.
Hardening Checklist
-
Validate provider-returned email against provider's verification status. Always consume the
emailVerifiedor equivalent flag from the OAuth token response. Useif (!$provider_response['email_verified']) { wp_die(); }before writing the email towp_insert_user(). -
Fetch and cache the provider's email verification status in user meta. Store the timestamp and verification flag in
update_user_meta()so that you can audit and revoke logins from providers whose verification standards degrade over time. -
Use WordPress nonces on the callback handler. Wrap the OAuth redirect URI handler in
wp_verify_nonce()to prevent CSRF attacks that force users into malicious social login flows. Example:check_admin_referer('social_login_action'). -
Implement account linking — do not auto-register. Require existing WordPress users to explicitly link their social account in their profile settings before allowing social login. This prevents an attacker from claiming a user's email via a social provider.
-
Log all social login failures with the provider response. Use
error_log()or a dedicated audit table to record rejected logins, including the provider, the email claimed, and the verification status returned. This enables post-incident analysis of attack attempts.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-9501