CSRF — Cross-site request forgery
Status: 🟩 COMPLETE Last updated: 2026-06-19 Plain-English tagline: Tricking a user’s browser into making a request to your site while they’re logged in — so the request goes through with their auth, but they didn’t intend it. Cookies are the reason this exists; SameSite is most of the defense.
In plain English
Cross-Site Request Forgery (CSRF) exploits a quirk of how browsers handle cookies: when you visit any site, your browser automatically sends along any cookies that belong to that site. So if you’re logged into your bank, and then a malicious site convinces your browser to make a request to your bank’s transfer endpoint, your bank cookies go along with the request automatically. From the bank’s perspective, the request looks legitimate — it has the right cookies — even though you never intended to make it.
Classic example: you log into your bank. You then visit evil.com in another tab. Evil.com has hidden code:
<img src="https://yourbank.com/transfer?to=attacker&amount=10000">Your browser sees the <img>, makes a GET request to that URL, includes your bank cookies, and the bank processes the transfer. You never clicked anything; the attacker just needed you to visit their page while logged in.
The fix is twofold:
- Modern browsers default to
SameSite=Laxfor cookies — they don’t send cookies on most cross-site requests anymore. - Apps add CSRF tokens — unique, per-session values that legitimate forms include and attackers can’t predict.
Like XSS, CSRF is one of the oldest web attacks. Modern frameworks (Next.js Server Actions especially) make it harder to be vulnerable. But it’s not gone — anywhere you have authenticated state-changing endpoints, you need to think about CSRF.
Why it matters
- Affects every authenticated state-changing endpoint. Anywhere a logged-in user can cause something to happen with a request, CSRF is theoretically possible.
- No code execution needed. Unlike XSS, CSRF doesn’t require running JavaScript on your site. Just luring your user to a page somewhere else.
- High-impact attacks. Money transfers, account changes, posting content, account deletion.
- Modern browsers help but aren’t a complete fix. Old browsers without SameSite, edge cases, configuration errors — still happen.
How CSRF works (in detail)
The exploit relies on three facts:
- Cookies attach automatically. When the browser makes a request to
yourbank.com, it adds the bank’s cookies regardless of who initiated the request. - HTML can initiate cross-site requests.
<img src>,<form action>,<script src>, even<link href>all cause requests to arbitrary URLs. - The server can’t tell where the request “came from.” A GET request from your tab vs. one triggered by an evil iframe looks identical to the server.
So attacker:
- Crafts a page (or email, or ad) with hidden form/image targeting your endpoint
- Convinces user to load the page while authenticated
- User’s browser fires the request
- Your server processes it as if user intended it
SameSite cookies — the modern default defense
Modern browsers introduced the SameSite cookie attribute that controls when cookies are sent cross-site:
| Value | Behavior |
|---|---|
Strict | Cookie only sent for same-site requests. Most secure; breaks some legitimate flows (e.g. coming from search engine results to a logged-in page). |
Lax | Cookie sent on top-level navigation (clicking a link to your site) but NOT on cross-site sub-requests (images, forms loaded from another origin). Modern default. |
None | Cookie sent on all cross-site requests. Requires Secure. Use only when needed. |
Lax is the modern browser default if no SameSite is set. This single change has made CSRF much harder by default — most CSRF patterns rely on the cookie being attached to a cross-site request, which Lax blocks.
Set your auth cookies to SameSite=Lax explicitly. See Sessions & cookies.
CSRF tokens — the application-layer defense
Even with SameSite, defense in depth requires CSRF tokens. Pattern:
- Server generates a random token, ties it to the user’s session
- Server includes the token in legitimate forms (
<input type="hidden" name="csrf_token" value="abc123...">) or as a custom HTTP header - Server requires the token on every state-changing request
- Attacker can’t read the token (same-origin policy) so can’t include it in forged requests
CSRF tokens block CSRF even without SameSite. Belt + suspenders is the right approach.
How Next.js handles CSRF
Server Actions
When you use Next.js Server Actions:
async function transferMoney(formData: FormData) {
"use server";
// ...
}
<form action={transferMoney}>...</form>Next.js automatically:
- Issues a per-session CSRF token
- Includes it in the form
- Verifies it on submission
You don’t write CSRF code. Server Actions are CSRF-safe by default.
Route Handlers (custom API routes)
// app/api/transfer/route.ts
export async function POST(request: Request) {
// ...
}These are NOT automatically CSRF-protected. You either:
- Verify SameSite cookies are in use (defense)
- Add CSRF token verification yourself
- Use a library like
csrfor@nestjs/csrf
For most webapps, prefer Server Actions over custom API routes for state-changing operations.
fetch from your own client
Calling your own API from your React components passes the same-origin check by default and automatically includes cookies. Cross-site requests to your API from another origin require explicit CORS allowance, so they don’t happen accidentally.
A concrete example: a CSRF attack
A user logged into gallery.app. Attacker hosts:
<!-- evil.com/seems-legit.html -->
<h1>Cute Cats!</h1>
<form id="csrf" action="https://gallery.app/api/delete-account" method="POST">
<input type="hidden" name="confirm" value="yes">
</form>
<script>document.getElementById('csrf').submit();</script>When the logged-in user visits evil.com:
- Browser loads the page
- JavaScript auto-submits the form
- Browser POSTs to
gallery.app/api/delete-accountwith the user’s session cookie attached gallery.appdeletes the account
Defenses that would block this:
SameSite=Laxcookie → cookie not sent on cross-site POST → server can’t authenticate the request- CSRF token → form lacks the legitimate token → server rejects
- Both, ideally
Modern stacks usually have both.
CSRF vs XSS — the distinction
| Aspect | CSRF | XSS |
|---|---|---|
| Attack vector | Tricked browser sends request | Attacker JS runs in your page |
| Requires user to | Visit attacker’s site while logged in | Visit your page (with injected content) |
| Damage | Whatever the request can do | Anything JS can do |
| Server can detect? | Look for missing token or wrong origin | Often invisible |
| Mitigation | SameSite, CSRF tokens, Origin/Referer checks | Output escaping, CSP, no dangerouslySetInnerHTML |
| Modern framework default | Often handled (Server Actions) | React escapes by default |
Often confused; they’re different attacks with different fixes. Sometimes both work together — XSS to bypass CSRF tokens (the injected script can read tokens from the page).
Other CSRF defenses
Origin / Referer header checks
On state-changing endpoints, verify the Origin header (or Referer) matches your domain. Attackers’ cross-site requests will have a different origin.
Caveats:
- Origin header isn’t always present (older browsers, some clients)
- Use as one of several checks, not sole defense
Double-submit cookie
A token is set in a cookie AND submitted as a form field. Server verifies they match. Works without server-side session state.
Custom request headers
Browsers can’t easily make cross-site requests with custom headers (CORS preflight blocks it). Requiring a custom header like X-Requested-With: XMLHttpRequest blocks many CSRF attempts.
Sensitive operations require re-authentication
For very sensitive ops (password change, money transfer): re-prompt for password regardless. Defense for when other layers fail.
CSRF in APIs (JWT-based auth)
If your API uses JWT in Authorization: Bearer headers rather than cookies, CSRF is much harder to exploit:
- Tokens in headers aren’t sent automatically by the browser
- Attacker would need XSS to read the token from localStorage / memory
This is one reason mobile apps using bearer tokens have lower CSRF risk. The trade-off: headers + localStorage = vulnerable to XSS instead.
There’s no free lunch in auth security. Pick the trade-offs deliberately.
Common gotchas
-
Trusting
GETrequests for state changes. Some servers process state changes via GET (e.g./delete?id=123). Even with cookies SameSite=Lax, top-level GET requests still send cookies. NEVER do state-changing work in response to GET requests. Always POST/PUT/DELETE. -
Forgetting CSRF protection on custom API routes. Server Actions are CSRF-safe; raw Route Handlers aren’t. Mixing them in one app means uneven protection.
-
SameSite=Nonewithout thinking. Sometimes needed (e.g. embedded widgets, OAuth). But it re-enables CSRF as a vector. Use sparingly. -
CSRF tokens stored only in cookies. If both the cookie and the form field come from the same source (cookie), attacker can copy the cookie and forge requests. Tokens need to be unguessable AND tied to the session.
-
Tokens that don’t change per session. A long-lived token leaked once stays valid forever. Rotate.
-
Hardcoded CSRF tokens in tests. Sometimes leak into production. Use environment-aware token generation.
-
Origin header check too loose. Allowing wildcards or treating empty origin as same-origin defeats the check.
-
Verifying only on some endpoints. Attackers target the weakest endpoint. Apply CSRF protection uniformly.
-
CORS misconfiguration.
Access-Control-Allow-Origin: *withAllow-Credentials: trueis forbidden by spec but some setups try; security implications are bad. -
CSRF via subdomain. If
a.example.comis compromised (XSS), it can read cookies set for.example.comand forge CSRF. Scope cookies tightly. -
Login CSRF. Less famous variant: attacker logs YOU into THEIR account, then can read what you do “as them.” Mitigation: same token mechanism.
-
CSRF in single-page apps via auth headers. If your SPA uses
Authorizationheaders (not cookies), classic CSRF is moot. But XSS becomes worse — attacker JS can read tokens. -
Long-lived tabs. User opened your bank yesterday, tab still open. Today they visit evil.com. Bank session might still be alive. Short sessions help.
-
Magic links and OAuth callbacks. These have their own attack vectors that resemble CSRF. Validate redirect URLs.
-
Re-using nonce/token across forms. Reduces protection. Per-form tokens are safer for high-value actions.
-
CORS preflight bypass. Some attackers craft requests that don’t trigger preflight (simple requests with only
Content-Type: application/x-www-form-urlencodedand no custom headers). Don’t rely on CORS preflight alone. -
Forgetting CSRF for password reset. Password reset is state-changing. Same rules apply.
-
Trusting Referer header alone. Privacy modes strip Referer. Some browsers omit it. Use Origin in addition.
See also
- XSS 🟩 — sibling attack, sometimes combined
- SQL injection 🟩 — another injection attack
- OWASP top 10 🟩 🟦 — CSRF formerly its own category, now grouped under A01 (Broken Access Control) in 2021 list
- Sessions & cookies 🟩 — SameSite is the main defense
- JWT 🟩 — bearer tokens reduce CSRF risk (with different trade-offs)
- Authentication vs authorization 🟩
- Passwords & hashing 🟩
- Secrets management 🟩
- Forms & validation 🟩 — Server Actions are CSRF-safe
- Next.js 🟩 🟦 — Server Actions handle CSRF automatically
- Glossary: CORS, Cookie