Skip to content

CVE-2026-11387: Verified Reproduction

CVE-2026-11387: SMS Alert WordPress plugin <= 3.9.5 allows unauthenticated attackers to change a user’s email and reset their password, leading to account takeover and privilege escalation when OTP password reset verification is enabled.

CVE-2026-11387 is verified against sms-alert · WordPress plugin (hosted on WordPress.org SVN, not GitHub). Affected versions: <= 3.9.5. Fixed in 3.9.6. Vulnerability class: Privilege Escalation. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00242.

REPRO-2026-00242 sms-alert · WordPress plugin (hosted on WordPress.org SVN, not GitHub) Privilege Escalation Jul 6, 2026 CVE entry .txt
Severity
CRITICAL
CVSS
9.8
Confidence
HIGH
Reproduced in
22m 56s
Tool calls
321
Spend
$7.05
01 · Overview

What Is CVE-2026-11387?

CVE-2026-11387 is a critical improper-authentication vulnerability (CWE-287, CVSS 9.8) in the SMS Alert WordPress plugin that lets an unauthenticated attacker change a user's email and reset their password, leading to full account takeover including administrators. Pruva reproduced it (reproduction REPRO-2026-00242).

02 · Severity & CVSS

CVE-2026-11387 Severity & CVSS Score

CVE-2026-11387 is rated critical severity, with a CVSS base score of 9.8 out of 10.

CRITICAL threat level
9.8 / 10 CVSS base
Weakness CWE-287 (Improper Authentication) — Improper Authentication

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

03 · Affected Versions

Affected sms-alert Versions

sms-alert · WordPress plugin (hosted on WordPress.org SVN, not GitHub) versions <= 3.9.5 are affected.

How to Reproduce CVE-2026-11387

$ pruva-verify REPRO-2026-00242
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00242/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-11387

Privilege escalation — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

Unauthenticated POST to wp-login.php?action=lostpassword with user_login=<admin_phone>&wc_reset_password=true (sets session), then POST with option=smsalert-change-password-form&smsalert_user_newpwd=<attacker_pwd>&smsalert_user_cnfpwd=<attacker_pwd> (resets password without OTP)

Attack chain
  1. wp-login.php?action=lostpassword
  2. lostpassword_post hook
  3. WPResetPassword::startSmsalertResetPasswordProcess()
  4. smsalert_site_challenge_otp() sets $_SESSION['user_login']
  5. POST ?option=smsalert-change-password-form
  6. WPResetPassword::routeData()
  7. handleSmsalertChangedPwd()
  8. reset_password($admin, $attacker_pwd)
How the agent worked 749 events · 321 tool calls · 23 min
23 minDuration
321Tool calls
191Reasoning steps
749Events
1Dead-ends
Agent activity over 23 min
Support
25
Hypothesis
2
Repro
285
Judge
26
Variant
407
0:0022:56

Root Cause and Exploit Chain for CVE-2026-11387

Versions: ≤ 3.9.5

The SMS Alert – SMS & OTP for WooCommerce WordPress plugin (versions ≤ 3.9.5) contains an unauthenticated privilege escalation vulnerability in its OTP-based password reset handler. The WPResetPassword::routeData() method in handler/forms/class-wpresetpassword.php calls handleSmsalertChangedPwd() whenever the request parameter option equals smsalert-change-password-form, without verifying that the OTP challenge was actually completed/validated. Because smsalert_site_challenge_otp() sets $_SESSION['user_login'] to the target user's login at OTP initiation time (before the OTP is sent), an attacker who triggers the OTP challenge for an administrator's phone number can immediately submit the password-change form and reset that administrator's password — bypassing OTP verification entirely. This leads to full account takeover and privilege escalation.

  • Package: SMS Alert – SMS & OTP for WooCommerce, Order Notifications & Abandoned Cart Recovery (slug: sms-alert)
  • Affected versions: ≤ 3.9.5
  • Patched version: 3.9.6
  • Risk level: Critical (CVSS 9.8)
  • Consequences: An unauthenticated remote attacker can reset any WordPress user's password (including administrators) when the SMS Alert plugin has OTP password-reset verification enabled and the target user has a billing_phone set. The attacker gains full administrative access to the WordPress site. The same class of vulnerability exists in the UltimateMember integration (class-ultimatemember.php, option smsalert-um-reset-pwd-action).

Impact Parity

  • Disclosed/claimed maximum impact: Unauthenticated privilege escalation via arbitrary password reset — attacker changes admin email/password and takes over the account.
  • Reproduced impact from this run: Unauthenticated attacker resets the administrator's password directly (without needing email change) by exploiting the missing OTP validation check. The attacker can then log in as administrator with the new password.
  • Parity: full — the core claimed impact (unauthenticated admin account takeover via arbitrary password reset) was demonstrated end-to-end against a running WordPress + WooCommerce + SMS Alert service.
  • Not demonstrated: The advisory describes an email-change → standard-password-reset path as one exploitation variant. Our reproduction demonstrates a more direct path: the password itself is reset via reset_password() without any email change or OTP validation, which is the same root cause (missing identity/OTP-validation check in routeData()).

Root Cause

Vulnerable code (class-wpresetpassword.php v3.9.5)
public function routeData()
{
    if (! empty($_REQUEST['option']) && sanitize_text_field(wp_unslash($_REQUEST['option'])) === 'smsalert-change-password-form' ) {
        $this->handleSmsalertChangedPwd($_POST);
    }
}

routeData() is called on every init hook (via FormInterface::__construct()add_action('init', ...)) and is therefore reachable by unauthenticated users. When option=smsalert-change-password-form is present, handleSmsalertChangedPwd() is invoked, which reads $_SESSION['user_login'] and calls WordPress's reset_password($user, $new_password) directly.

How $_SESSION['user_login'] gets set

The session variable is set by smsalert_site_challenge_otp() in handler/smsalert_form_handler.php:

function smsalert_site_challenge_otp(...) {
    SmsAlertUtility::checkSession();
    $_SESSION['user_login']      = $user_login;   // <-- set at INITIATION, not validation
    $_SESSION['phone_number_mo'] = $phone_number;
    _handle_otp_action(...);                       // <-- sends OTP; may fail
}

This function is called from WPResetPassword::startSmsalertResetPasswordProcess() (hooked to WordPress's lostpassword_post action) when a user submits the lost-password form with a phone number and wc_reset_password=true. The session variables are set before the OTP is sent via the SMS Alert API. Even if the OTP API call fails (invalid credentials, network error), the session already contains the target user's login.

Missing validation check

In the vulnerable version, handle_post_verification() (called after successful OTP validation) does not set $_SESSION[$this->form_session_var] to 'validated'. And routeData() does not check for this value. Therefore, the password reset proceeds regardless of whether the OTP was ever validated.

Fix (v3.9.6, changeset 3587983)

The patch adds two changes to class-wpresetpassword.php:

  1. routeData() now requires OTP validation:
public function routeData()
{
    SmsAlertUtility::checkSession();
    if (! empty($_REQUEST['option']) && (sanitize_text_field(wp_unslash($_REQUEST['option'])) === 'smsalert-change-password-form') && isset($_SESSION[ $this->form_session_var ]) && strcasecmp($_SESSION[ $this->form_session_var ], 'validated') === 0 ) {
        $this->handleSmsalertChangedPwd($_POST);
    }
}
  1. handle_post_verification() now marks the session as validated:
$_SESSION[ $this->form_session_var ] = 'validated';

The same fix pattern was applied to class-ultimatemember.php (option smsalert-um-reset-pwd-action, session var form_session_var2).

Fix reference

Reproduction Steps

  1. Script: bundle/repro/reproduction_steps.sh
  2. What the script does:
    • Installs PHP, MariaDB, and wp-cli
    • Downloads and installs WordPress 7.0 + WooCommerce 10.9.3
    • Installs SMS Alert plugin 3.9.5 (vulnerable) and configures it: sets non-empty gateway credentials (so is_user_authorised() returns true), enables reset_password=on, and sets the admin user's billing_phone to 919999990001
    • Starts a PHP built-in web server on localhost:8080
    • Phase A (vulnerable): Sends an unauthenticated POST to wp-login.php?action=lostpassword with user_login=919999990001&wc_reset_password=true — this triggers lostpassword_poststartSmsalertResetPasswordProcess()smsalert_site_challenge_otp() which sets $_SESSION['user_login']='admin'. The OTP API call fails ("Wrong SMSAlert credentials") but the session is already poisoned. Then sends a second unauthenticated POST with option=smsalert-change-password-form&smsalert_user_newpwd=Pwned!Pass42&smsalert_user_cnfpwd=Pwned!Pass42 — this calls handleSmsalertChangedPwd() which reads the session and calls reset_password($admin, 'Pwned!Pass42'). The server returns HTTP 302 redirect to ?password-reset=true.
    • Verifies via wp user check-password that the new password works and the original does not
    • Phase B (patched negative control): Repeats the exact same attack against SMS Alert 3.9.6 — the password change is blocked (HTTP 200, no redirect), and the original password still works
  3. Expected evidence:
    • Vulnerable 3.9.5: HTTP 302 with Location: ?page_id=8&password-reset=true; wp user check-password admin 'Pwned!Pass42' succeeds; original password fails
    • Patched 3.9.6: HTTP 200 (no redirect); wp user check-password admin 'HackedPass99' fails; original password still works

Evidence

Log file locations
  • bundle/logs/reproduction_steps.log — full script output
  • bundle/logs/exploit_vuln_step1_response.html — OTP challenge response (vulnerable)
  • bundle/logs/exploit_vuln_step2_headers.txt — password change response headers showing 302 redirect
  • bundle/logs/exploit_vuln_cookies.txt — session cookies used in the exploit
  • bundle/logs/exploit_fixed_step2_headers.txt — patched version response (200, no redirect)
  • bundle/repro/runtime_manifest.json — structured runtime evidence
Key excerpts

Vulnerable 3.9.5 — Step 2 response (password reset confirmed):

HTTP/1.1 302 Found
X-Redirect-By: WordPress
Location: http://localhost:8080/?page_id=8&password-reset=true

Vulnerable 3.9.5 — Password verification:

New password 'Pwned!Pass42'       → OK   (expected: OK)
Old password 'admin_original_123' → FAIL (expected: FAIL)

Patched 3.9.6 — Step 2 response (attack blocked):

HTTP/1.1 200 OK
(no redirect — attack blocked)

Patched 3.9.6 — Password verification:

Hacked password 'HackedPass99'      → FAIL (expected: FAIL)
Original password 'admin_original_123' → OK   (expected: OK)
Environment
  • PHP 8.5.4 (built-in server)
  • MariaDB 11.8.6
  • WordPress 7.0
  • WooCommerce 10.9.3
  • SMS Alert 3.9.5 (vulnerable) / 3.9.6 (patched)

Recommendations / Next Steps

  1. Upgrade immediately to SMS Alert plugin version 3.9.6 or later.
  2. Audit all routeData() handlers across the plugin's form classes for similar missing OTP-validation checks. The same pattern was found and fixed in class-ultimatemember.php.
  3. Defense in depth: Consider adding nonce verification and capability checks to all password-modification endpoints, not just session-state checks.
  4. Testing: Add integration tests that verify password reset requires a validated OTP session before allowing handleSmsalertChangedPwd() to execute.

Additional Notes

  • Idempotency: The script was run twice consecutively; both runs produced identical results (vulnerable exploit succeeded, patched control blocked). The script is designed to be idempotent — it reuses existing WordPress installs and reconfigures the plugin on each run.
  • Preconditions: The vulnerability requires (a) OTP password reset enabled (smsalert_general[reset_password]=on), (b) the plugin authorized (smsalert_gateway credentials non-empty), (c) WooCommerce active, and (d) the target user having a billing_phone set. These match the advisory's stated preconditions.
  • No real SMS credentials needed: The exploit works even when the SMS Alert API credentials are invalid, because smsalert_site_challenge_otp() sets the session variables before the OTP API call, and handleSmsalertChangedPwd() does not check OTP validation status.
  • Session cookie handling: The exploit uses PHP's native session mechanism (PHPSESSID cookie). Both HTTP requests (OTP challenge + password change) must share the same PHPSESSID cookie.

CVE-2026-11387 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:001:52
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-11387 · REPRO-20
0:03
0:05
web search
0:06
web search
0:09
0:10
1:11
1:14
1:15
web search
1:16
web search
1:20
1:20
1:20
error

Unknown error

1:22
1:23
1:35
1:35
extract_facts
no facts extracted
1:36
1:36
1:36
supportrepro
1:48
1:50
1:50
1:50
1:51
1:51
1:51
1:51
1:51
1:52
08 · How to Fix

How to Fix CVE-2026-11387

Upgrade sms-alert · WordPress plugin (hosted on WordPress.org SVN, not GitHub) to 3.9.6 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-11387 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-11387

How does the CVE-2026-11387 account takeover work?

An unauthenticated attacker triggers the SMS Alert OTP password-reset flow for an administrator account that has a billing_phone set, which populates $_SESSION['user_login'] with the admin's login. The attacker then submits the password-change form directly, bypassing OTP verification, and resets the administrator's password to gain full administrative access to the WordPress site.

Which SMS Alert plugin versions are affected by CVE-2026-11387, and where is it fixed?

SMS Alert - SMS & OTP for WooCommerce, Order Notifications & Abandoned Cart Recovery versions <= 3.9.5 are affected. It is fixed in 3.9.6.

How severe is CVE-2026-11387?

It is rated critical severity with a CVSS score of 9.8: an unauthenticated remote attacker can reset any WordPress user's password, including administrators, when OTP password-reset verification is enabled and the target has a billing phone set.

How can I reproduce CVE-2026-11387?

Download the verified script from this page and run it in an isolated environment against WordPress with SMS Alert <= 3.9.5 installed and OTP password-reset enabled. It triggers the OTP challenge for a target user without providing the OTP code, then submits the password-change form directly and confirms the password was reset on the vulnerable build.
11 · References

References for CVE-2026-11387

Authoritative sources for CVE-2026-11387 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.