{
  "equivalence_confidence": "high",
  "parent_root_cause": "SMS Alert's OTP-initiation function smsalert_site_challenge_otp() writes $_SESSION['user_login'] (and the per-form transaction session var) BEFORE the OTP is sent/validated. The password-change dispatch (WPResetPassword::routeData() for option=smsalert-change-password-form) consumes that initiation-time session state to call handleSmsalertChangedPwd() -> reset_password() WITHOUT requiring proof that the OTP was ever validated (no check that the form_session_var equals 'validated').",
  "variant_root_cause": "Same mechanism via a different handler: UltimateMember::handleForm() dispatches option=smsalert-um-reset-pwd-action to handleSmsalertChangedPwd() -> reset_password() WITHOUT checking that $_SESSION['SA_UM_RESET_PWD'] equals 'validated'. The session is poisoned at initiation by UltimateMember::smsalertUmResetPwdSubmitted() (hooked to um_reset_password_process_hook) -> startOtpTransaction() -> smsalert_site_challenge_otp(), which sets $_SESSION['user_login'] before the OTP API call.",
  "shared_sink": "handleSmsalertChangedPwd() -> WordPress reset_password($user, $new_password), where $user = get_user_by('login', $_SESSION['user_login']).",
  "shared_session_poisoning_function": "smsalert_site_challenge_otp() in handler/smsalert_form_handler.php (sets $_SESSION['user_login'] at initiation, before OTP validation).",
  "difference": "Entry point / dispatch only. Parent: WPResetPassword::routeData() (WooCommerce/wp-login, option=smsalert-change-password-form, session var wp_default_lost_pwd). Variant: UltimateMember::handleForm() (UltimateMember, option=smsalert-um-reset-pwd-action, session var SA_UM_RESET_PWD). Different plugin dependency, different action hook (lostpassword_post vs um_reset_password_process_hook), different option value, different session key — same missing-validation-check bug and same reset_password() sink.",
  "same_root_cause": true,
  "same_sink": true,
  "same_trust_boundary": true,
  "fix_coverage": "The 3.9.6 fix (changeset 3587983) applies the identical 'validated' gate to BOTH handlers, so the variant is covered (not a bypass). Verified at runtime: variant reproduces on 3.9.5, blocked on 3.9.6.",
  "evidence": {
    "vulnerable_reproduced": true,
    "fixed_blocked": true,
    "repro_log": "logs/vuln_variant_reproduction.log"
  }
}
