Abstract. WP Hide & Security Enhancer (100,000+ active installations) wraps the WordPress login in a two-factor authentication (2FA) layer. In version 2.9.3, under the plugin's default configuration, the 2FA challenge handler trusts a client-controlled "Set up later" signal and finishes the login of a user who has already configured a second factor — no code is ever checked. We treat the root cause as a secure-coding case study in authentication-state handling and give site owners concrete detection and hardening steps. This is a fresh (0-day) finding, unpatched as of writing. The vendor has been notified under coordinated disclosure, and the full operational proof of concept stays private until a fix ships.
Keywords: WordPress security, two-factor authentication bypass, broken authentication, missing critical step, CWE-287, CWE-304, secure coding.
1. Why a 2FA bypass matters
A second factor earns its place at the exact moment the first factor has failed. By the time an attacker is at the login form with a working password — phished, reused, leaked, or guessed — 2FA is the only control still standing. Let the same attacker skip it and the feature delivers reassurance, not protection.
WP Hide & Security Enhancer is a widely deployed WordPress hardening plugin with more than 100,000 active installations. One of its features intercepts the login after the username/password step and presents a second-factor challenge — an e-mail code, an authenticator-app code, or a recovery code — before any authentication cookie is issued.
What makes this worth writing up is how ordinary the mistake is. There is no cryptographic weakness here and no exotic edge case: an authentication-state logic error sits on the most security-critical path of a security plugin, and it is reachable in the plugin's default configuration. Finishing an authentication flow while a mandatory step has quietly been skipped is among the most common and most damaging bugs in authentication code, and the pattern reaches well past this one plugin.
2. Affected component
| Plugin | WP Hide & Security Enhancer — slug wp-hide-security-enhancer |
| Tested version | 2.9.3 (current/latest at time of testing) |
| Active installations | 100,000+ |
| Vulnerability type | Two-Factor Authentication Bypass / Broken Authentication |
| CWE | CWE-287 Improper Authentication (CWE-304 Missing Critical Step in Authentication) |
| Required privilege | Unauthenticated attacker who possesses the victim's primary credentials (username + password). 2FA must be enabled for the victim's role with the default Enforce User to Configure 2FA = No. |
| User interaction | None |
| CVSS 3.1 | 6.8 (Medium) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H, reduced for the credential precondition (triager may adjust) |
| Status | Fresh (0-day), reproduced end-to-end in an isolated lab on the current version; unpatched at publication |
3. Root-cause analysis
Once the password step succeeds, the plugin holds back the authentication cookie, shows its own 2FA form, and routes login through a dedicated handler. The intent is correct: no wordpress_logged_in cookie should exist until a second factor has been verified.
The handler goes wrong in how it treats the "Set up later" affordance. That path is there for genuine first-time enrolment — a user who has just had 2FA required of them, but has not yet configured any option, may be let through once while they set it up, provided the administrator has not made configuration mandatory. To decide whether to honour the path, though, the handler reads the wrong piece of state.
Here is the relevant branch in modules/components/login_2fa.php, login_form_validate_2fa() (v2.9.3):
$_2fa_require_setup = $this->wph->functions->get_module_item_setting('2fa_require_setup'); // default 'no'
$other_2fa_option_require_setup = $this->_2fa_get_first_option_require_setup( $user->ID );
if ( strtolower( $submit ) == 'setuplater' && $_2fa_require_setup !== 'yes' ) {
// Only redirects if SOME OTHER active option still needs first-time setup:
foreach ( $this->active_2fa_options as $active_2fa_option_id => $_2fa_option ) {
if ( $active_2fa_option_id === '2fa_recovery_codes' ) continue;
if ( $_2fa_option->user_2fa_require_setup()
&& ! $_2fa_option->user_2fa_option_setup_completed( $user->ID )
&& $_2fa_id != $active_2fa_option_id ) {
wp_safe_redirect( $this->login_url( ... ) ); exit();
}
}
// No option needs setup -> log the user in WITHOUT verifying any 2FA code:
$this->login_success( $args ); // -> wp_set_auth_cookie()
exit;
}Walking through why the logic breaks:
- Nowhere does the branch ask whether the current user still needs to set up 2FA. It only redirects when some different option is incomplete. Someone whose own active factor already works is not the user "Set up later" was written for — yet the code cannot tell the two apart.
- The e-mail option reports
user_2fa_require_setup()asFALSE(anduser_2fa_option_setup_completed()asTRUE), and the authenticator-app option reportsuser_2fa_option_setup_completed()asTRUEonce enrolled. For a user with a configured or primary option, the loop body is therefore never true and control falls straight intologin_success(). login_success()then callswp_set_auth_cookie()with no code check at all.- There is a separate guard,
authenticate_block_cookies(), hooked on theauthenticatefilter, which normally suppresses the auth cookie until 2FA passes. It runs only when that filter runs, and the 2FA-validation request path never invokeswp_authenticate. So the backstop never fires, and the cookielogin_success()sets is genuinely delivered. 2fa_require_setupdefaults to'no'(modules/components/login_2fa_defaults.php), which leaves the vulnerable branch active out of the box.
At bottom this is a textbook missing critical step (CWE-304). The trust decision answers "does some other option need setup?" when the security-relevant question is "has this user's second factor been verified, or do they truly have none configured?" The two questions coincide for a brand-new user — which is exactly why the intended case works — and they split apart for the users 2FA is meant to protect, which is why it fails.
4. How the flaw is reached (conceptual)
The attacker needs nothing more than what 2FA already assumes is in enemy hands: the victim's username and password. Making that knowledge insufficient is the whole job of the second factor.
The password step runs normally. It validates the first factor and, because the plugin is active, withholds the authentication cookie and returns the second-factor form. The account is still protected here — a session-less challenge, and no code to answer it.
The break comes one step later. When the challenge handler decides whether it is safe to finish login without a code, it reads the setup state of other 2FA options rather than whether this user's own factor was actually verified. If the victim's active factor needs no first-time setup — e-mail, which always counts as configured, or an authenticator app enrolled earlier — that precondition is satisfied permanently. The "Set up later" branch concludes there is nothing left to configure and finalises login, issuing the cookie without a single code being checked.
The second factor collapses into a no-op for precisely the people it exists to defend: those who already have it configured. Nothing is guessed, no token is stolen, no race is won. The handler simply reaches a "log them in" sink down a branch that should never have been open to an already-configured account. While the plugin remains unpatched we are deliberately not publishing the literal request sequence, parameter values, or reproduction commands.
5. Impact assessment
Out of the box, anyone holding a user's password can skip the entire 2FA layer. That is the precise threat 2FA exists to stop — credential compromise through phishing, reuse, or leakage — so the control fails in the one scenario it was bought for.
Every account with 2FA enabled is affected, administrators included. A successful bypass grants full authenticated access to the targeted account; for an administrator that is effectively full control of the site. Confidentiality, integrity, and availability impact on the targeted account are all high.
The CVSS base metrics describe a network-reachable, low-complexity, no-interaction flaw with high C/I/A impact. The published 6.8 (Medium) reflects a downward adjustment for the credential precondition — the attacker must already hold a valid password. That precondition is also why the finding matters: defeating 2FA for a password-holding attacker is the entire point of the feature. A triager may weight it differently.
6. Remediation
Tie the trust decision to the authenticating user's own second-factor state, not to the setup state of unrelated options:
// Treat the user's own configured/primary option as "must be verified".
// Only allow "Set up later" when the CURRENT user genuinely has NO usable,
// configured 2FA option — not merely when some OTHER option is incomplete.
$current_option = $this->active_2fa_options[ $_2fa_id ] ?? null;
$user_has_usable_factor =
$current_option
&& $current_option->user_2fa_option_setup_completed( $user->ID );
if ( strtolower( $submit ) == 'setuplater'
&& $_2fa_require_setup !== 'yes'
&& ! $user_has_usable_factor ) {
// genuine first-time enrolment only -> allowed to proceed
...
} else {
// a configured factor exists -> REQUIRE and verify its code before login_success()
...
}For maintainers and operators:
- In the
setuplaterbranch, allow the skip only when the authenticating user genuinely has no usable configured 2FA option. Beforelogin_success(), confirm the user's own active/primary option does not already have a completed setup — count a configured e-mail or app option as "setup complete" and demand its code. - A client-supplied
submit=setuplaterfield is not authorization to finish login for a user who already has a working second factor. "Set up later" belongs to genuine first-time enrolment only, and the check belongs on the server. - Apply the auth-cookie guard that suppresses the login cookie until 2FA passes on the 2FA-validation path too, so no single handler can set the cookie out of band.
- Update to a fixed release as soon as the vendor ships one. Until then, use the hardening options below.
7. Detection and hardening for site owners
This is an authentication-state logic flaw, not a payload-driven attack, so detection lives in login-flow anomalies rather than signatures:
- Enforce 2FA configuration. The vulnerable branch is gated on the default
Enforce User to Configure 2FA = No. Flipping it to Yes changes how the "Set up later" path behaves and is sound hardening in its own right — mandatory enrolment is good practice regardless. - Watch for logins that never consumed a second factor. Where 2FA is enabled, a successful authenticated session with no preceding verified e-mail/app code is anomalous. Correlate 2FA-challenge issuance with 2FA-success events; an authenticated cookie without a matching verification is worth a look.
- Alert on unexpected administrator sessions. New admin logins from unusual locations, user agents, or hours deserve scrutiny, all the more while this flaw is unpatched.
- Shrink what a stolen password buys. The bug only helps an attacker who already has a valid password. Enforce strong, unique passwords, reset any credentials known or suspected to be reused or exposed, and keep the count of administrator accounts small.
- Virtual patching / WAF. A web application firewall or request-filtering layer can, in principle, flag or block requests to the 2FA-validation endpoint that try to finalise login via a "set up later" signal while the account has an active configured factor. Treat it as a stopgap that buys time, not a replacement for the code fix.
- Keep the plugin updated. Apply the vendor's fixed release promptly once it exists, and subscribe to the plugin's security advisories.
None of these are invented product features; they follow from the mechanics of the flaw and ordinary WordPress operational practice.
8. The broader lesson
An authentication flow has to make its final trust decision from the state of the step being enforced — not from a neighbouring, unrelated one. "May the user finish logging in without a second factor?" is a question about this user's second factor. Answering it by inspecting whether some other option needs setup is a category error: right for the benign case, wrong for the adversarial one.
Three secure-coding habits would have caught it:
- Default to deny, then carve exceptions narrowly. The code defaulted to "finish login" and redirected only on a specific condition. The safer shape requires verification and skips it only under a tightly scoped, server-verified first-time-enrolment condition.
- Never let a client-supplied field stand in for an authorization decision.
submit=setuplatercomes from the browser. Treating it as permission to complete login hands a security-critical choice to the attacker. - Make guards unconditional on every path to the sink. The cookie-suppression backstop existed but did not run where it mattered. A control that applies on only some routes to the sink is not a control.
Authentication code is where "works in testing" and "is secure" are easiest to confuse. Treat every branch that can issue a session cookie as security-critical, and verify the enforced step itself right there.
9. Responsible disclosure
This finding is handled under coordinated disclosure. The vendor has been notified privately and in good faith, and the full operational proof of concept — the literal request sequence, parameter values, and reproduction commands — is withheld from this public article until a fix is available. The write-up exists to help site owners and developers understand, prevent, and detect the bug class, not to enable exploitation.
All verification was carried out by the Noroxi LLC security research team against an isolated, locally-bound test installation. No third-party or production systems were accessed, and nothing beyond our own lab was involved. We will coordinate the timing of any further technical detail, and the public availability of a fix, with the vendor.
References
- CWE-287: Improper Authentication — https://cwe.mitre.org/data/definitions/287.html
- CWE-304: Missing Critical Step in Authentication — https://cwe.mitre.org/data/definitions/304.html
- OWASP Authentication Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- OWASP Multifactor Authentication Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html
- WordPress Plugin Developer Handbook: Security — https://developer.wordpress.org/plugins/security/
- FIRST CVSS v3.1 Calculator — https://www.first.org/cvss/calculator/3.1
Published for defensive and educational purposes. Noroxi LLC conducts authorized security research only; operational exploit detail is withheld until a fix is available.
Exploited this week · every Monday
New KEV entries, fresh PoCs, WordPress triage and what to do this week. No account needed.