# REPRO-2026-00242: 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. ## Summary Status: published Severity: critical Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00242 CVE: CVE-2026-11387 ## Package Name: sms-alert Ecosystem: WordPress plugin (hosted on WordPress.org SVN, not GitHub) Affected: <= 3.9.5 Fixed: 3.9.6 ## Root Cause # RCA Report — CVE-2026-11387 ## Summary 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. ## Impact - **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) ```php 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`: ```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:** ```php 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); } } ``` 2. **`handle_post_verification()` now marks the session as validated:** ```php $_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 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_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 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. ## Reproduction Details Reproduced: 2026-07-06T08:32:50.450Z Duration: 1376 seconds Tool calls: 321 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00242 pruva-verify CVE-2026-11387 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00242&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00242/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-11387 - Source: https://nvd.nist.gov/vuln/detail/CVE-2026-11387 ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 16128 bytes) - bundle/repro/rca_report.md (analysis, 10479 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 16673 bytes) - bundle/vuln_variant/rca_report.md (analysis, 10931 bytes) - bundle/artifact_promotion_manifest.json (other, 13675 bytes) - bundle/vuln_variant/source_identity.json (other, 1937 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 2364 bytes) - bundle/repro/runtime_manifest.json (other, 1092 bytes) - bundle/repro/validation_verdict.json (other, 1203 bytes) - bundle/logs/exploit_vuln_step2_headers.txt (other, 365 bytes) - bundle/logs/exploit_fixed_step2_headers.txt (other, 352 bytes) - bundle/logs/reproduction_steps.log (log, 4278 bytes) - bundle/logs/patch_diff_wpresetpassword.txt (other, 1128 bytes) - bundle/logs/exploit_vuln_step1_response.html (other, 30230 bytes) - bundle/logs/exploit_vuln_step2_response.html (other, 0 bytes) - bundle/logs/exploit_vuln_cookies.txt (other, 278 bytes) - bundle/logs/exploit_fixed_step1_response.html (other, 30234 bytes) - bundle/logs/exploit_fixed_step2_response.html (other, 74436 bytes) - bundle/logs/exploit_fixed_cookies.txt (other, 278 bytes) - bundle/logs/php-server.log (log, 603 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 7606 bytes) - bundle/vuln_variant/variant_manifest.json (other, 4449 bytes) - bundle/vuln_variant/validation_verdict.json (other, 1274 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1620 bytes) - bundle/logs/vv_vuln_step2_headers.txt (other, 369 bytes) - bundle/logs/vv_fixed_step2_headers.txt (other, 352 bytes) - bundle/logs/vuln_variant_reproduction.log (log, 2215 bytes) - bundle/logs/vv_vuln_step1.html (other, 34288 bytes) - bundle/logs/vv_vuln_step1_headers.txt (other, 710 bytes) - bundle/logs/vv_vuln_step2.html (other, 0 bytes) - bundle/logs/vv_vuln_cookies.txt (other, 200 bytes) - bundle/logs/vv_fixed_step1.html (other, 34288 bytes) - bundle/logs/vv_fixed_step1_headers.txt (other, 710 bytes) - bundle/logs/vv_fixed_step2.html (other, 83102 bytes) - bundle/logs/vv_fixed_cookies.txt (other, 200 bytes) - bundle/logs/vv-php-server.log (log, 4306 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00242 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00242/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00242 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev