Two attack classes, one theme: breaking the gate. CSRF exploits the browser's automatic trust — it sends cookies whether you want it to or not. Authentication bugs exploit the gate itself — weak login flows, predictable tokens, no rate limits, MFA that can be skipped. Together they account for a massive slice of paid bug bounty reports.
CSRF is an attack that tricks an authenticated user's browser into sending an HTTP request they didn't intend to make. The server receives the request with the victim's valid session cookie attached — and has no way to distinguish it from a legitimate action.
The core mechanic: browsers attach cookies to every request for a domain, regardless of where the request originates. A page on evil.com can force a request to bank.com, and the victim's browser dutifully sends the session cookie along with it.
The most dangerous pattern: a web app uses GET requests to perform actions that change data. Any HTML tag that fetches a URL can trigger it silently.
Attacker's page — victim loads this while logged into bank.com<!-- Option A: image tag — zero interaction needed -->
<img src="https://bank.com/transfer?to=attacker&amount=5000">
<!-- Option B: invisible iframe -->
<iframe src="https://bank.com/transfer?to=attacker&amount=5000" style="display:none"></iframe>
<!-- Option C: link the victim clicks -->
<a href="https://bank.com/delete-account">Click for free gift</a>
Most sensitive actions use POST. Slightly more setup required — but a hidden form that auto-submits on page load is all it takes.
Auto-submitting CSRF PoC — the victim just has to visit this page<html>
<body onload="document.forms[0].submit()">
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker_account">
<input type="hidden" name="amount" value="10000">
</form>
</body>
</html>
The server embeds a secret, unpredictable token in every form. On submission it checks the token matches. An attacker can't read it cross-origin (Same-Origin Policy), so they can't include it in the forged request.
Properly protected form<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="a3f8e2c1d9b746f...">
<input name="amount" value="100">
</form>
In theory, airtight. In practice — full of holes. Here's the bypass catalogue:
POST → GET. Many frameworks don't re-validate on method change. If the endpoint accepts both methods, no token needed.Content-Type: application/x-www-form-urlencoded. Switching to text/plain or application/json bypasses the check if the endpoint accepts multiple content types.?csrf=TOKEN), it leaks in Referer headers to any third-party resource on the page (analytics, CDNs). Attacker reads it from server logs.Browsers added SameSite to address CSRF at the transport level — controlling when cookies travel cross-site.
| Value | Behavior | CSRF Protection | Bypass |
|---|---|---|---|
Strict |
Cookie never sent on any cross-site request, including top-level link clicks from external sites | Maximum | Subdomain attacks only |
Lax |
Cookie sent on top-level navigations (link clicks) but NOT on cross-site sub-resources (images, AJAX, iframes, forms) | Strong | GET-based state changes; legacy browser gaps |
None; Secure |
Cookie sent on ALL cross-site requests — required for embedded iframes, OAuth, payment widgets | None | Classic CSRF works in full |
| (absent) | Chrome defaults to Lax since 2020. Older browsers treat as None | Partial | Older browsers; GET-based actions |
Lax still allows cookies on GET top-level navigations (the user clicks a link). If the server accepts state-changing GET requests, a crafted link is enough. CSRF via GET works even with SameSite=Lax. This is why using GET for state changes is a bug on its own, regardless of CSRF token presence.
<meta name="referrer" content="no-referrer"> to their page.if "target.com" in referer. Attacker registers evil-target.com or creates path evil.com/target.com/page. The string is present — check passes.Toggle defenses and fire the forged request. Watch the server log. Each combination teaches a real attack scenario.
Developers often assume REST APIs accepting application/json are immune to CSRF because HTML forms can't set a custom Content-Type. This is false in two ways:
Form-based CSRF against a JSON endpoint using text/plain enctype<form action="https://api.target.com/transfer"
method="POST"
enctype="text/plain">
<!-- name becomes the JSON key, value becomes the rest -->
<input name='{"to":"attacker","amount":1000,"padding":"'
value='"}'>
</form>
<!-- Resulting body sent to server: -->
{"to":"attacker","amount":1000,"padding":"="}
If the server parses this as JSON while ignoring the trailing =, the attack works. Additionally — if the endpoint accepts application/x-www-form-urlencoded alongside JSON (common in loose frameworks), standard form CSRF works directly.
Authentication bugs are weaknesses in the "who are you?" layer — the login mechanism, password reset flows, MFA implementation, and session lifecycle. Unlike CSRF (which abuses a working session), these bugs break the gate itself.
Applications inadvertently reveal which accounts exist through different responses to login attempts. Different error messages are the most obvious form — but timing side-channels are harder to spot and just as exploitable.
| Enumeration Method | What Leaks | How to Test |
|---|---|---|
| Different error messages | "Incorrect password" vs "Account not found" — text directly confirms valid accounts | Try known-valid username vs random strings. Compare response body text. |
| Response timing | Valid usernames trigger a password hash computation (slow). Invalid usernames return early (fast). Measurable latency difference even with identical error messages. | Burp Intruder → Columns → Response Received. Sort by time. Valid accounts cluster at higher values. |
| Password reset page | "Reset email sent to admin@..." vs "No account found" — confirms the address | Submit reset for known accounts vs made-up ones. Compare responses. |
| Account registration | "Username already taken" confirms the account exists | Try registering common usernames: admin, root, support, api, test. |
| Response size difference | Identical error text but different whitespace, field counts, or JSON keys | Burp Intruder → Columns → Response Length. Different lengths = different responses. |
Summer2024! across all confirmed usernames), and targeted phishing.
Watch how different usernames produce different server responses — and how rate limiting (when present) shuts down brute-force. Toggle the rate limit and spam attempts to see both behaviors.
When Rate Limit is ON, rotate this value to bypass IP-based lockout.
Even when rate limiting exists, implementation flaws commonly allow attackers to bypass it entirely.
X-Forwarded-For as the real client IP. Header is user-controlled. Rotate it on each request — each attempt appears to come from a different IP. No lockout ever triggers.
X-Forwarded-For: 1.1.1.1 → attempt 1
X-Forwarded-For: 1.1.1.2 → attempt 2fail×4 → login as self → fail×4 → repeatSummer2024!, Company123!. After enumerating valid usernames, try one guess per account per hour.Reset flows are multi-step, complex, and written once — then forgotten. That combination breeds bugs.
Some frameworks build the password reset URL using the Host request header. An attacker intercepts the reset request and substitutes their own domain:
The victim receives an email pointing to attacker.com. When they click it, the reset token lands on the attacker's server. Use Burp Collaborator as the Host value to detect this — any DNS/HTTP ping to your Collaborator URL confirms the vulnerability.
token=1699123456 — Unix timestamp at request time. Predictable within seconds of receiving the email.token=00042 — Request a reset, get token 42. Request again, get 43. Enumerate to find any user's active token.token=admin_reset_2024 — Trivially guessable for known usernames.Test method: Request resets for your own account at known times. Check if the token correlates to a timestamp or sequential value. Request multiple tokens in quick succession — if they're incrementing, the model is broken.
/reset?token=SECRET. If that page loads any third-party resources, the token leaks in the Referer header to those servers.MFA bugs are among the highest-paying authentication findings. The assumption that "MFA = secure" makes them easy to overlook — and expensive when they're found.
Select an attack vector and trace how MFA can be bypassed at each step of the authentication flow.
/mfa/verify step altogether./mfa/verify. Change "mfa_passed": false to "mfa_passed": true. Apps that trust client-side logic redirect to the dashboard without the server ever confirming a valid code./api/users/42. Or returned in plaintext in the account settings response. Check every profile-related endpoint.Testing MFA skip — send this after credentials verify, before /mfa/verify# Step 1: POST /login with valid credentials
# Server sets: Set-Cookie: session=PARTIAL_TOKEN; Path=/
# Step 2: Skip POST /mfa/verify entirely
# Attempt: GET /dashboard with the partial token
GET /dashboard HTTP/1.1
Cookie: session=PARTIAL_TOKEN
# If 200 OK — MFA step is not enforced server-side
# If 302 /mfa — MFA correctly enforced
Authentication bugs don't end at login. The post-login session lifecycle has its own distinct vulnerability class.
Representative findings from public HackerOne/Bugcrowd disclosures. Study the attack chain — this is what reports look like.
/mfa/verify response and changing "mfa_passed": false to true bypassed the second factor. Full account access with just a username and password.Host header. Attacker submits reset with Host: attacker.com. Victim receives email with link to attacker's server. Reset token captured. Password reset to attacker's choice. Detected with Burp Collaborator.X-Forwarded-For header. Rotating the header value per request allowed unlimited attempts. Combined with username enumeration (different error messages) — full credential brute-force pipeline.Profile update, password change, email change, fund transfer, account deletion, permission grants, API key generation. Every action that modifies data is a CSRF candidate.
Is the action POST/PUT/DELETE — or GET? GET-based state changes are automatically higher severity. Test if changing POST to GET on an action still works.
DevTools → Application → Cookies. Is SameSite present? If None or absent, cookies travel cross-site. CSRF is mechanically possible regardless of token status.
Remove the token entirely — does the request succeed? Change token to garbage — does it succeed? Try a valid token from a different session — does it succeed? Any "yes" is a bug.
Burp → right-click request → Engagement Tools → Generate CSRF PoC. Modify parameter values. Open in browser while logged in as test account. Confirm action executes. Screenshot each step for the report.
Try known-valid vs known-invalid usernames at login, password reset, and registration. Compare: response body text, status codes, response length, and response time.
Send 10+ failed login attempts. Does the server lock you out? If yes — try rotating X-Forwarded-For, X-Real-IP, CF-Connecting-IP headers on each attempt.
Request resets at known times (timestamp tokens). Request multiple in sequence (incrementing tokens). Check if old tokens still work after new ones are generated. Intercept with Burp Collaborator in the Host header.
Skip the /mfa/verify step. Intercept and modify the MFA response. Try OTP brute-force on test accounts. Test code reuse within 30-second windows. Check backup code endpoints for IDOR.
Log out — replay request with old cookie. Change password — replay with pre-change cookie. Both should reject. If either accepts, report it. Note session ID before and after login — if unchanged, session fixation.
| Tool | Purpose | Key Usage |
|---|---|---|
| Burp Suite | Intercept, modify, replay all requests | Right-click → Generate CSRF PoC. Intruder for brute-force. Collaborator for Host header detection. |
| Burp Intruder | Automated credential testing | Pitchfork mode for credential pairs. Cluster Bomb for small username×password matrices. Check Response Length column for enumeration. |
| DVWA | Local CSRF and brute-force lab | CSRF module at Low/Medium/High. Brute force module. Essential for building PoC skills before live targets. |
| Burp Collaborator | Out-of-band detection | Use as the Host header value in password reset requests. Any DNS/HTTP callback confirms Host header injection. |
| PortSwigger Labs | Guided hands-on practice | CSRF labs 1–8. Authentication labs including enumeration, brute-force, MFA bypass series. |
Explain in plain terms why the browser attaches cookies to cross-site requests. What specific browser behavior does CSRF exploit, and why can't the target server distinguish a forged request from a real one?
Name three scenarios where a CSRF token is present in the form but the protection still fails. For each, describe what the attacker does to bypass it.
Username enumeration is typically a Low finding. Describe a full attack chain where enumeration is the first step leading to a Critical or High severity outcome. What additional conditions enable the escalation?
Walk through the Host Header injection attack on a password reset endpoint step by step. What server-side condition makes this possible, and how would you detect it without the victim actually clicking the link?
You're testing an app with MFA. The OTP endpoint has rate limiting. The response body can't be manipulated client-side. What other MFA bypass vectors would you test, and in what order?