Modern web applications have evolved well beyond static HTML forms. With client-heavy SPAs, complex authentication flows, and increasingly mature security controls, today’s testers face different challenges than a decade ago. One of the most prevalent security upgrades across platforms is the adoption of two-factor authentication (2FA). While this adds an extra hurdle for attackers, misconfigurations and flawed implementations still leave many systems vulnerable.
In this post, we’ll break down common pitfalls in 2FA implementations, how to test for them, and where things usually go wrong. This isn’t a general overview of 2FA — this is for pentesters looking to break it.
Session Timing: Before or After 2FA?
One of the most common architectural choices you’ll see is whether a session starts before or after the 2FA step. For example:
POST /login HTTP/1.1
Host: target.com
Content-Type: application/json
{
"username": "victim@domain.com",
"password": "correcthorsebatterystaple"
}
HTTP/1.1 200 OK
Set-Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6...; Path=/; HttpOnly
{
"2fa_required": true,
"message": "Please complete second factor"
}
Notice the session is issued immediately after the password check, but before 2FA is verified. This opens the door for several issues if routes aren’t properly guarded.
Unprotected Routes After Initial Login
A classic misstep is failing to restrict sensitive endpoints before the 2FA step is complete. For example, if /account/settings or even worse, an action route like /transactions/initiate, is accessible right after step one, you’ve effectively bypassed the purpose of 2FA.
Manually test this by replaying a request with the session cookie obtained pre-2FA:
GET /account/settings HTTP/1.1
Host: target.com
Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6...
If you get a 200 OK and access to sensitive data, that’s a problem. Proper implementations should gate access using middleware or backend checks that validate that 2FA has been completed.
Prematurely Exposed Props
Another variation of this mistake is leaking sensitive data in the 2FA prompt response. For example:
HTTP/1.1 200 OK
{
"2fa_required": true,
"user_data": {
"email": "victim@domain.com",
"role": "admin",
"pending_transactions": [...]
}
}
This tells an attacker more than they need to know. If you can brute-force usernames, you can start harvesting metadata — roles, account status, maybe even token balances — before 2FA is solved.
2FA Code Bruteforce & Race Conditions
Many systems implement custom 2FA where the server verifies the code like this:
POST /2fa/verify HTTP/1.1
Host: target.com
Content-Type: application/json
Cookie: session=...
{
"code": "123456"
}
If there’s no rate limiting or lockout, you can brute-force the code, especially for time-based ones with predictable formats (e.g., 6 digits, 30-second rotation). Test by submitting codes rapidly in sequence and monitor responses:
Does the response time change?
Does it block after N attempts?
Does it validate codes that shouldn’t yet be valid?
Brute-forcing can also work via race conditions. If the server doesn’t properly handle concurrency, sending multiple valid codes in parallel may result in one of them succeeding even if others are expired or invalid.
Missing Rate Limit at Issuing Codes
For systems that send SMS or email codes, another weak point is the issuance itself:
POST /2fa/send HTTP/1.1
Host: target.com
Cookie: session=...
{
"method": "sms"
}
Try sending this repeatedly. If there’s no throttling (e.g., 1 per 30 seconds or per minute), you can easily prompt spam to the user (annoyance vector) or flood the delivery pipeline.
Recovery Codes Not Rotated After Use
Applications often provide backup codes in case you lose access to your device. But if a used code isn’t invalidated, you can reuse it:
POST /2fa/verify HTTP/1.1
Host: target.com
Cookie: session=...
{
"recovery_code": "ABCD-1234-EFGH"
}
Use Burp’s Repeater to replay the request. If the server returns a 200 OK more than once for the same code, it’s vulnerable.
Prompt Bombing (Push Notification)
Apps that rely on push-based 2FA (e.g., approve/deny prompts) can be abused by repeatedly triggering login attempts. This leads to “prompt fatigue,” where users might approve a request by habit.
Simulate this attack by replaying login attempts and triggering multiple push notifications within a short window. No need to compromise the device — user psychology can work in your favor.
2FA Code Leaks in HTTP Responses
Sometimes devs forget to clean up debug data or temporary test responses. I’ve seen 2FA codes returned in error objects, stack traces, or even embedded in HTML like:
<!-- DEBUG: 2FA code = 492381 -->
Scrape 2FA-related responses for these accidental leaks. Especially common in dev/test environments.
Weak Recovery Methods
Security questions like “What’s your favorite color?” are not 2FA. If the fallback method is trivial or publicly guessable, it nullifies the entire second factor.