You visit a site that seems harmless — a forum post, a shortened URL, an image in an email. Behind the scenes, your browser silently sends a request to your bank, your admin panel, or your email provider — authenticated with your session cookie — and performs an action you never intended. That’s CSRF.
TL;DR
- CSRF exploits the fact that browsers automatically attach cookies to every request, including cross-origin ones
- An attacker tricks the victim into sending a forged request while logged in to a target site — no credentials needed
- Impact ranges from profile changes to fund transfers, password resets, and account takeover
- The primary defense is CSRF tokens — unique, unpredictable values that must accompany state-changing requests
- SameSite cookie attributes provide strong additional protection but are not a standalone fix
Why CSRF Works
Browsers have a fundamental behavior: when you visit any web page and it sends a request to bank.example.com, the browser automatically includes any cookies stored for bank.example.com — regardless of which site initiated the request.
This was intentional design. It’s what makes “remember me” sessions, persistent logins, and seamless navigation work. But it also means that if an attacker can get your browser to send a request to a site you’re authenticated on, the server sees a fully authenticated request — and it came from you.
The attacker doesn’t need to steal your session token. They just need your browser to send a request at the right moment, to the right URL, with the right parameters.
A Simple CSRF Attack
Imagine a banking application with an endpoint:
POST /transferamount=5000&to_account=12345The server checks: is the user’s session cookie valid? Yes. Transfer approved.
An attacker builds a malicious page:
<form id="csrf" action="https://bank.example.com/transfer" method="POST"> <input type="hidden" name="amount" value="5000" /> <input type="hidden" name="to_account" value="attacker_account" /></form><script>document.getElementById("csrf").submit();</script>When a victim visits evil.example.com/steal.html while logged into their bank:
- The hidden form auto-submits to
bank.example.com/transfer - The browser attaches the bank’s session cookie automatically
- The server processes it as a legitimate transfer
- $5,000 moves to the attacker’s account
The victim sees nothing. The page loads, the script runs, the form submits — all in milliseconds before the page even renders visually.
Types of CSRF Attacks
GET-Based CSRF
Some applications use GET requests for state-changing actions (a violation of HTTP semantics, but it happens). These are the easiest to exploit — a single <img> tag is sufficient:
<img src="https://target.com/admin/delete-user?id=42" style="display:none">When the victim’s browser loads the page, it requests the image URL. The GET request executes the action. The browser sends the session cookie. The user is deleted.
POST-Based CSRF
The most common type. Requires a form submission, but that can be automated with JavaScript:
<form action="https://target.com/change-email" method="POST"> <input type="hidden" name="email" value="attacker@evil.com" /></form><script>document.forms[0].submit();</script>JSON-Based CSRF
Modern APIs often accept JSON. This is trickier — the browser’s HTML form mechanism can’t set Content-Type: application/json natively. But there are workarounds:
Option 1: If the server accepts application/x-www-form-urlencoded or multipart/form-data in addition to JSON:
<form action="https://api.target.com/profile" method="POST" enctype="text/plain"> <input name='{"email":"attacker@evil.com","ignore":"' value='"}' /></form>Option 2: If the server doesn’t validate Content-Type strictly, a fetch request from a third-party page with no-cors mode will send cookies in some configurations.
Login CSRF
A less-obvious variant: the attacker forges a login request with their own credentials. If the victim’s browser completes the login, the victim is now authenticated as the attacker’s account — and any sensitive data they enter (including credit card information on a shopping site) goes to the attacker’s account.
Real-World Impact Scenarios
Account Takeover via Email Change
- Victim is logged into a service
- Attacker sends a phishing email with a link to
evil.example.com - The malicious page fires a CSRF request:
POST /settings/emailwithemail=attacker@evil.com - Victim’s email is changed to attacker’s address
- Attacker requests a password reset — gets the email — takes over the account
Admin Action Abuse
Victim is a system administrator, logged into an admin panel. Attacker sends the victim a message containing an image with a CSRF payload. The image request triggers an admin action: creating a new admin account for the attacker, or deleting audit logs.
OAuth and Token Theft via CSRF
CSRF against OAuth callback endpoints can let attackers hijack the OAuth flow and link their account to the victim’s identity. This is a known attack against poorly implemented OAuth integrations.
Finding CSRF Vulnerabilities
Step 1: Identify State-Changing Requests
CSRF only matters for state-changing actions — things that modify data:
- Changing email, password, username
- Making payments or transfers
- Deleting accounts or data
- Adding/removing roles or permissions
- Any admin actions
GET requests that only return data are generally not CSRF targets (though GET-based state changes are a separate problem).
Step 2: Check for CSRF Tokens
Capture a state-changing request in Burp Suite. Look for:
- A hidden form field with a random-looking value:
<input type="hidden" name="_csrf" value="a3f9c2..."/> - A request header:
X-CSRF-Token: a3f9c2... - A cookie paired with a matching request parameter
If there’s no token — test directly. If there is a token — move to bypass testing.
Step 3: Test CSRF Token Validation
Many implementations are flawed. Test each bypass systematically:
| Test | Method |
|---|---|
| Remove the token entirely | Delete the _csrf parameter |
| Submit an empty token | _csrf= |
| Submit a different user’s valid token | Use token from Account B on Account A’s request |
| Submit a completely made-up token | _csrf=aaaaaaaaaaaa |
| Change the request method | Switch from POST to GET with same parameters |
If any of these return 200 OK and process the action — CSRF protection is broken.
Step 4: Check SameSite Cookie Settings
In browser DevTools or Burp Suite, examine the Set-Cookie headers for session cookies:
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnlySameSite=Strict— cookie is never sent on cross-origin requests. Strong protection.SameSite=Lax— cookie sent on top-level navigations (clicking links) but not form submits orfetch. Moderate protection.SameSite=None— no cross-origin protection. Cookie is sent everywhere.- No
SameSiteattribute — browser defaults (Lax in modern browsers, but varies)
If the session cookie has SameSite=None or no SameSite at all, CSRF is viable.
Step 5: Build the Proof of Concept
Create a minimal HTML file that auto-submits the forged request:
<!DOCTYPE html><html><body><form id="csrf" action="https://target.com/api/change-email" method="POST"> <input type="hidden" name="email" value="attacker@test.com" /></form><script>document.getElementById("csrf").submit();</script></body></html>Open the file locally in a browser while logged into the target. If the action executes — the vulnerability is confirmed.
Bypass Techniques
Bypassing Referer/Origin Checks
Some applications validate the Referer or Origin header instead of using tokens.
Bypass 1: Remove the header entirely — some servers accept requests with no Referer:
POST /transfer HTTP/1.1Host: bank.example.comCookie: session=abc123# No Referer headerBypass 2: Host a malicious page at a URL that contains the target domain:
https://evil.com/?https://bank.example.comThe Referer will contain bank.example.com as a substring — a loose substring check might pass.
Bypass 3: Use an attacker-controlled subdomain:
https://bank.example.com.evil.com/csrf.htmlBypassing CSRF Tokens via XSS
If the application has an XSS vulnerability, CSRF tokens become irrelevant — JavaScript can read the token from the page and include it in the forged request:
// Attacker's XSS payload reads the CSRF token and sends itconst token = document.querySelector('input[name="_csrf"]').value;fetch('/change-email', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: `email=attacker@evil.com&_csrf=${token}`});This is one reason XSS and CSRF are often chained: XSS defeats CSRF tokens, CSRF exploits the authenticated session.
Bypassing Double-Submit Cookie Pattern
The double-submit pattern is: set a cookie with a random value, include the same value as a form parameter. The server checks they match. This works — unless the attacker can write cookies for the target domain.
If the attacker controls any subdomain of the target (sub.example.com), they can set a cookie for .example.com, then submit a forged request with a matching parameter value. The check passes.
Bypassing JSON CSRF Restrictions
Some APIs only accept Content-Type: application/json, assuming HTML forms can’t set this. But:
<form action="https://api.target.com/profile" method="POST" enctype="text/plain"> <!-- The body becomes: {"email":"attacker@evil.com","pad":"=ignored"} --> <input name='{"email":"attacker@evil.com","pad":"' value='ignored"}' /></form>If the server parses the body as JSON without strict content-type validation, this works.
Fixing CSRF: Developer Guidance
Defense 1: CSRF Tokens (Primary Fix)
Generate a cryptographically random token per session (or per request). Include it as a hidden field in every state-changing form. Validate it server-side on every state-changing request.
# Generate on session startimport secretssession["csrf_token"] = secrets.token_hex(32)
# Include in every form<input type="hidden" name="csrf_token" value="{{ session.csrf_token }}" />
# Validate on every POSTdef validate_csrf(): token = request.form.get("csrf_token") if not token or token != session.get("csrf_token"): abort(403)The token must be:
- Unpredictable — cryptographically random, minimum 128 bits
- Session-tied — different for each user session
- Validated server-side — not just presence-checked but value-verified
Defense 2: SameSite Cookies
Set SameSite=Strict or SameSite=Lax on all session cookies:
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnlySameSite=Strict is the strongest setting — the cookie is never sent on cross-origin requests, including when clicking a link from another site. This means users will need to re-authenticate after clicking external links to your site, which can hurt UX.
SameSite=Lax is a good balance — cookies are sent on top-level navigations (clicks) but not on subresource requests from other origins. This blocks form-submit CSRF and fetch-based CSRF but allows normal link navigation.
Important: SameSite alone is not sufficient if the attacker controls a subdomain of your domain.
Defense 3: Origin/Referer Validation (Secondary)
Check that requests come from your own origin:
def check_origin(): origin = request.headers.get("Origin") or request.headers.get("Referer") if origin and not origin.startswith("https://yourapp.example.com"): abort(403)This is a defense-in-depth measure, not a primary control — it can be bypassed in some configurations and should never be the only CSRF protection.
Defense 4: Custom Request Headers for APIs
APIs can require a custom header that simple HTML forms can’t set:
X-Requested-With: XMLHttpRequestCORS policy prevents cross-origin JavaScript from setting arbitrary headers without a preflight. So if your API requires X-Requested-With, an attacker’s HTML form can’t satisfy it.
Limitation: This doesn’t protect against subdomain XSS — JavaScript on a subdomain can set arbitrary headers.
Defense 5: Require Re-Authentication for Sensitive Actions
For high-impact actions (changing passwords, transferring funds, deleting accounts), require the user to re-enter their password. Even if CSRF bypasses token checks, the attacker can’t supply the current password.
CSRF vs. SSRF vs. XSS — Clearing Up the Confusion
| Vulnerability | Who sends the request | Request goes to | What’s exploited |
|---|---|---|---|
| CSRF | Victim’s browser | Application server | Browser automatically sends cookies |
| SSRF | Application server | Internal/external services | Server makes requests on attacker’s behalf |
| XSS | Victim’s browser | Anything (from attacker’s JS) | Injected script runs in victim’s browser context |
All three involve unauthorized requests — but they exploit different trust relationships and require different defenses.
What You Can Do Today
If you’re a developer:
- Add CSRF tokens to every state-changing form —
<input type="hidden" name="csrf" value="{{ token }}" /> - Set
SameSite=LaxorSameSite=Stricton all session cookies — this is a one-line change in most frameworks - For APIs: require the
X-Requested-Withheader or implement proper CSRF token verification - Never use GET requests for state-changing actions
If you’re a security tester:
- Intercept every state-changing POST request — check for CSRF tokens in parameters or headers
- Test the 4 basic bypasses: remove token, empty token, wrong token, method switch
- Check
Set-Cookieheaders for missingSameSiteattributes — it’s an instant finding - If XSS exists anywhere, CSRF tokens are moot — document the chain
If you’re a manager:
- Add CSRF token verification to your security checklist for code reviews
- Ensure security testing covers state-changing actions specifically
- Run a cookie audit — session cookies missing
SameSite,Secure, andHttpOnlyflags are a compliance finding in most frameworks
Related Posts
- XSS Explained: How Attackers Inject Code Into Your Browser — XSS defeats CSRF tokens; these two vulnerabilities are frequently chained
- IDOR Explained: How Attackers Access Anyone’s Data by Changing a Number — sister access control vulnerability in the web attack cluster
- SSRF Explained: How Attackers Make Servers Fetch Secrets for Them — another server-side trust vulnerability; different attack class, similar root causes
- API Security in 2026: JWT Attacks, OAuth Abuse, and GraphQL Exploitation — CSRF in API contexts, including JSON CSRF and OAuth flow attacks
- Web Application Penetration Testing 2026: Beyond OWASP Top 10 — full web testing methodology hub