2FA bypass: how it works, how pentesters test it and how to fix it
A 2FA bypass is any way to reach an account's post-login resources without a valid second factor. There are two families: logic flaws in the app, such as trusting the client's verification result or not limiting OTP attempts, and phishing-class attacks such as MFA fatigue and reverse-proxy phishing. Fix the first with server-side state and attempt limits, and the second with phishing-resistant authenticators.
A 2FA bypass is any way to reach an account's post-login resources without a valid second factor. The cause is either a flaw in the application's second-step logic or an attack on the user that makes them hand over or approve the factor. Pentesters test the first kind directly. Defenders also need to plan for the second.
Testing needs written authorization from the system owner and test-owned accounts. This guide shows what to check and how to fix it. It does not cover attacking accounts you do not own.
What are the two families of 2FA bypass?#
The two families are logic flaws and phishing-class attacks. They need different tests and different fixes, so keep them apart in a report.
| Family | What breaks | Example | Fix |
|---|---|---|---|
| Logic flaw | The app's own second-step checks | The server trusts a client-side "code valid" result | Server-side state, attempt limits |
| Phishing-class | The user or the session | Push approval after repeated prompts, a reverse-proxy phishing page | Number matching, FIDO2 and WebAuthn |
PortSwigger's multi-factor lessons cover the first family, including flawed verification logic and brute-forcing codes. OWASP's multifactor cheat sheet covers both, and states the core rule: factor changes and resets must be no weaker than normal authentication.
How does a 2FA logic flaw work?#
A logic flaw appears when the server marks a session as "logged in" before the second factor is checked, or checks the factor in a way the client can influence. CWE-287 (Improper Authentication) and CWE-307 (Improper Restriction of Excessive Authentication Attempts) are the usual weakness classes.
Illustrative 2FA state flaw: Harbor Notes
This is a demonstration. No real customer system was tested. Harbor Notes is a fictional app and example.com is a reserved example domain.
A tester signs in as a test user with a known password. The server answers the password step with a session cookie and sends the browser to the code page. The tester requests a protected page directly, with the same cookie, before sending any code:
GET /api/v1/notes HTTP/1.1
Host: api.harbor-notes.example.com
Cookie: session=<cookie-issued-after-password-step>
A vulnerable server returns 200 and the notes of the test user. The session was already fully privileged. A safe server returns 401 or 403 with a body that says the second step is pending. The control is the same request with no cookie, and a third request after the real code is entered. The evidence is the difference between the three, saved with timestamps.
The same pattern covers three more flaws. The server may accept the code on one endpoint while another endpoint skips the check. A mobile API may not enforce the second step at all. The code check may be tied to a client-chosen user identifier instead of the session.
What are the main 2FA bypass variants and how do you test each?#
Testers check 12 variants. Each test runs on test accounts, and each fix is server-side.
| Variant | Test | Fix |
|---|---|---|
| Direct access to post-2FA pages | Request a protected page after the password step only | Session state "pending-2fa" blocks all protected routes |
| Response tampering | Change a failure response to a success in a proxy and see if the app continues | Never trust a client-side result, decide on the server |
| OTP not limited | Send a bounded number of wrong codes and watch for a lockout | Per-account limit that a new code does not reset |
| OTP not bound to user or session | Use a code issued for account A on account B | Bind the code to the user and the pending session |
| OTP reuse | Submit a used code again | Mark a code used on success |
| No expiry | Submit an old code after the validity window | Short validity, enforced on the server |
| Race on verify | Send the same valid code in parallel requests (see race conditions) | Atomic compare-and-consume |
| Backup or recovery code flaws | Check limits, entropy and reuse of recovery codes | Single-use codes, same limits as OTP |
| Reset path skips MFA | Reset the password on a test account with MFA enabled | Require the factor after reset (reset flaws) |
| Remember-me cookie | Check what the "trust this device" cookie protects | Sign and bind it to the user, expire it, revoke on password change |
| Alternate login routes | Try SSO, API tokens, mobile and legacy endpoints | One enforcement point for every route |
| Factor change without proof | Disable MFA or add a factor with only a session | Require a previously enrolled factor |
OWASP says that for a factor change, you should require proof with a factor the user already enrolled and "Do not rely solely on the active session, as it may be hijacked."
What are the real cases?#
Public CVEs show the logic family, and a well-documented incident shows the phishing-class family.
CVE-2022-45173 was published on 14 April 2023. NVD says a bypass of two-factor authentication in LIVEBOX Collaboration vDesk through v018, at the /api/v1/vdeskintegration/challenge endpoint, happened because only the client side verified whether the check was successful, so an attacker could modify the response and fool the application into concluding the TOTP was correct. The weakness is CWE-287. This is the response tampering row of the table.
CVE-2025-26522 was published on 14 February 2025. NVD says the RupeeWeb trading platform had an improper OTP validation mechanism in certain API endpoints, and a remote attacker with valid credentials could manipulate API responses to bypass 2FA for other user accounts. The attacker needed a valid account. The flaw crossed to other accounts.
For the phishing-class family, Uber's own security update describes an incident that began on 15 September 2022. An attacker had a contractor's credentials. Each login attempt sent the contractor a two-factor approval prompt. Uber says the prompts stopped the attacker at first, but "eventually, however, the contractor accepted one, and the attacker successfully logged in." Uber said it was strengthening its multi-factor authentication policies. This case is why OWASP asks for challenge-response push such as number matching.
How do phishing-class MFA attacks work?#
Phishing-class attacks work on the user, not on the server logic, and the server sees a valid factor. Three kinds matter for most teams.
- MFA fatigue: The attacker has the password and triggers repeated approval prompts until one is accepted. OWASP says to require challenge-response push (for example, number matching) and to cap the number of prompts.
- Reverse-proxy phishing: A fake site relays the real login page and captures the factor and the session. OWASP says FIDO2 and WebAuthn bind authentication to the legitimate origin, which defeats this attack.
- SIM swap and SMS interception: The attacker takes over the phone number. NIST SP 800-63B-4 classes SMS and voice as restricted and recommends checking SIM swap and number porting signals.
A pentest of your application cannot prove that your users resist phishing. It can show whether the app offers phishing-resistant factors, limits prompts, notifies users of new devices and ends sessions when a factor changes.
How do pentesters prove a 2FA bypass?#
A bypass is proven when a request reaches a post-authentication resource without a valid second factor, and a control shows the normal path blocks it. The method follows our methodology.
- Use two test accounts with MFA enabled. Record the enrollment state.
- Capture the full login sequence with timestamps: password step, code step, first protected request.
- Run the variant tests above one at a time. Each has a control request: no cookie, wrong code, another account's code.
- Read back with a second session: if a bypass changed data, fetch the record from another logged-in session.
- Use a real browser (Chromium) for response tampering and client-side checks, and label the finding "Browser-observed".
- State the bounds. Brute-force tests send a small, agreed number of wrong codes on a test account, and the report gives the number.
Report the controls that held. A lockout that works after the agreed number of wrong codes is a result worth recording, so a later change does not remove it.
How do you prevent 2FA bypass?#
Keep one server-side state machine for authentication, enforce it on every route and limit attempts. The code shape is the same in every language: a session starts as "password verified, second factor pending", and only a correct code changes it.
Node.js (Express):
function requireFullAuth(req, res, next) {
if (req.session?.auth !== "complete") return res.status(401).json({ error: "2fa_required" });
next();
}
app.post("/2fa/verify", async (req, res) => {
if (req.session?.auth !== "password_ok") return res.sendStatus(401);
const user = await users.get(req.session.userId); // user comes from the session, not the body
if (user.failedOtp >= 5) return res.status(429).json({ error: "locked" });
const ok = await otp.verifyAndConsume(user.id, String(req.body.code)); // atomic, single use
if (!ok) { await users.incFailedOtp(user.id); return res.sendStatus(401); }
await users.resetFailedOtp(user.id);
req.session.regenerate(() => { req.session.userId = user.id; req.session.auth = "complete"; res.sendStatus(204); });
});
Python (Flask style):
@app.post("/2fa/verify")
def verify():
if session.get("auth") != "password_ok":
abort(401)
user = User.get(session["user_id"]) # never trust a user id from the request
if user.failed_otp >= 5:
abort(429)
if not totp_verify_and_consume(user, request.form["code"]):
user.failed_otp += 1; db.commit(); abort(401)
user.failed_otp = 0; db.commit()
session.clear(); session["user_id"] = user.id; session["auth"] = "complete"
return "", 204
Java (Spring Security): add a filter after the password step that grants only a PRE_AUTH_2FA authority, and map every protected route to FULL_AUTH. Spring's authorization rules then reject a pending session on all routes, including the ones you forgot.
PHP (Laravel):
// routes: every protected group uses the 2fa-complete middleware
Route::middleware(['auth', 'two-factor.complete'])->group(function () { /* ... */ });
// middleware
public function handle($request, $next) {
if (! $request->session()->get('two_factor_passed')) {
return response()->json(['error' => '2fa_required'], 401);
}
return $next($request);
}
Rules that apply to every stack, taken from OWASP and NIST:
- Codes expire quickly, work once and are invalid after success. OWASP says to apply strict attempt limits.
- NIST SP 800-63B-4 says generating a new authentication secret must not reset the failed authentication count, and caps consecutive failures per authenticator at 100 or fewer. Set your own limit much lower.
- Treat MFA disable, email change and password change as high-risk, notify the user out of band and require a previously enrolled factor.
- Enforce MFA on every login path, including APIs and mobile apps, and shut off legacy endpoints that cannot enforce it.
- Prefer FIDO2 or WebAuthn. For passkeys used as a second factor, check the user verification flag on the server. OWASP says a touch that only confirms presence is not a second factor.
- Do not accept approve-only push. NIST says that method is no longer acceptable. Use number matching.
2FA bypass testing checklist#
Run this on a test environment with test accounts and written authorization.
- A protected route returns 401 or 403 after the password step and before the code.
- Changing a failed code response to success in a proxy does not grant access.
- A wrong-code limit exists per account and holds across sessions and new codes.
- A code for account A fails for account B and in another session.
- A used code and an expired code both fail.
- Parallel submissions of one valid code allow at most one success.
- Recovery codes are single use and limited like OTP.
- Password reset, SSO, API and mobile routes enforce the second factor.
- Disabling MFA or adding a factor needs a previously enrolled factor.
- Remember-device cookies are bound, expire and are revoked on password change.
- Push prompts are capped and use number matching, or the app offers WebAuthn.
- Users get a notice when a factor changes or a new device signs in.
What if you are locked out of your own account?#
Use the provider's official recovery path. That means saved recovery codes, another enrolled factor or the support process on the provider's own site. A bypass technique on an account you cannot prove you own is account takeover, and in most places it is illegal. If you build the product, give users a recovery path that is no weaker than normal login, as OWASP says.
When should you get a professional test?#
Get a professional test when accounts hold money, personal data or admin rights, when you add MFA to an existing app, or before a customer security review. Logic flaws depend on how your routes, mobile apps and recovery paths fit together, so a scanner cannot see them. Our web application penetration testing covers authentication, MFA and recovery with controls and read-back evidence. Excession Labs agrees the scope with you in writing before testing starts. For a wider scope, see the authentication testing checklist.
Need a pentest you can act on?
Tell us what you need tested and why. We reply to agree scope, and testing starts only after written authorization.
Questions
What is the difference between 2FA bypass and MFA bypass?
The terms mean the same thing in testing. 2FA is MFA with exactly two factors. Both describe getting past the second step. This page uses 2FA bypass for flaws in the application's second-step logic and MFA bypass for attacks on the user, such as push fatigue.
How do pentesters test OTP brute force?
They check whether the app limits attempts per account, whether a new code resets the counter, and whether the limit holds across sessions. They send a bounded number of wrong codes on a test account, agreed during scoping, and never try to guess a real user's code. NIST SP 800-63B-4 says a new secret must not reset the failed count.
Is SMS 2FA safe?
It is weaker than other options. NIST SP 800-63B-4 classes SMS and voice as restricted authenticators and recommends checking risk signals such as SIM swaps. OWASP's multifactor sheet says to prefer FIDO2 or TOTP over SMS against SIM swaps.
What should I do if I am locked out of my own account?
Use the provider's official recovery flow, such as saved recovery codes, a second enrolled factor or the support process on the provider's own site. Do not use bypass techniques on an account you cannot prove you own. Testing a bypass on any account needs the owner's written authorization.
Do passkeys stop 2FA bypass?
They stop phishing-class attacks, because a WebAuthn credential is bound to the site's origin. They do not fix logic flaws. If the app still has a fallback path without the passkey, a tester will use that path. Check every login and recovery route.
Sources
- OWASP Multifactor Authentication Cheat Sheet (checked 9 November 2026)
- PortSwigger Web Security Academy: Vulnerabilities in multi-factor authentication (checked 9 November 2026)
- NIST SP 800-63B-4: Authentication and Authenticator Management (checked 9 November 2026)
- CWE-287: Improper Authentication (checked 9 November 2026)
- CWE-307: Improper Restriction of Excessive Authentication Attempts (checked 9 November 2026)
- NVD: CVE-2022-45173 (LIVEBOX Collaboration vDesk) (checked 9 November 2026)
- NVD: CVE-2025-26522 (RupeeWeb trading platform) (checked 9 November 2026)
- Uber Newsroom: Security update (September 2022) (checked 9 November 2026)