Multi-factor authentication is one of the most effective controls a web application can deploy. But a control is only as strong as the point at which it's enforced. This writeup walks through a class of vulnerability where the MFA step looks mandatory in the browser, yet the server never actually requires it — allowing an attacker to walk straight past it. Everything below is presented on a generic, illustrative scenario for educational purposes.
How authentication is supposed to work
A typical login flow with a second factor looks like this:
- The user submits a valid username and password.
- The server verifies the credentials and issues a temporary, partially-authenticated session.
- The application asks for a one-time password (OTP).
- Only after the correct OTP is submitted does the server upgrade the session to fully authenticated and grant access to protected pages.
The critical word here is server. Each of these checks must be enforced on the backend. The browser only reflects the flow — it should never be the thing that decides whether a step was completed.
Where it breaks
The vulnerability appears when the application treats a valid username and password as sufficient to grant a fully privileged session, and relies on the front-end to "guide" the user to the OTP screen — rather than the server blocking access until the OTP is verified.
In other words, the OTP page is a signpost, not a gate. Nothing on the server stops a user from simply walking around it.
The session is elevated to full access after the password step. The OTP prompt is enforced only in the UI, so any request that skips it still carries an authorized session.
Steps to reproduce
On a vulnerable application, the flow to demonstrate the bypass is straightforward:
- Authenticate once normally and complete the OTP so the account is in a known-good state.
- Log out, then begin a fresh login with valid credentials.
- When the application presents the OTP prompt, do not submit a code.
- Instead, directly request a protected, post-authentication route — for example, navigating straight to the dashboard path in a new tab:
# Instead of completing the OTP step, request the protected route directly
GET https://app.example.com/dashboard
# If the server never enforced the OTP, the page loads —
# the session was already fully privileged after the password.
If the protected page renders and serves authenticated content, the second factor has been bypassed. The application verified something (the password) but never enforced the step that was supposed to make the login trustworthy.
Why this happens
This pattern shows up for a few recurring reasons:
- Full session issued too early. The session token is granted complete privileges right after the password check, before OTP verification.
- Client-side flow control. The redirect to the OTP screen is handled by the front-end, and no server-side guard exists on the protected routes.
- Missing state on the session. The server doesn't track whether the current session has actually cleared the second factor.
Impact
The impact is direct: the entire value of the second factor is nullified. An attacker who has obtained valid credentials — through phishing, credential stuffing, or a prior breach — can access the account without ever needing the victim's OTP, defeating the exact scenario MFA exists to protect against. Depending on the application, this can expose personal data, account functions, and any sensitive operations behind the login.
Remediation
The fix is to enforce the authentication state where it matters — on the server:
- Don't grant a full session until MFA passes. After the password step, issue only a limited, partially-authenticated session that cannot access protected resources.
- Track MFA completion server-side. Store a flag such as
mfa_verifiedon the session, and require it on every protected route. - Guard the routes, not just the UI. Every sensitive endpoint should independently verify that the session is fully authenticated — never assume the user "must have" passed a prior screen.
- Fail closed. If the MFA state is missing or ambiguous, deny access by default.
Authentication is a state the server must own and re-check on every protected request. If a security step can be skipped by requesting the next URL directly, it was never really enforced — it was only displayed.
Closing thought
The most valuable findings often aren't exotic exploits — they're places where a control that everyone assumed was working simply wasn't enforced at the right layer. Testing authentication means questioning every step: not "does the app ask for this?" but "what happens if I don't give it?"