CSRF (Cross Site Request Forgery) tutorial
By Flavio Copes
Learn how CSRF attacks make authenticated browsers send unwanted requests, and how tokens, SameSite cookies, and origin checks prevent them.
CSRF stands for Cross Site Request Forgery.
A CSRF attack tricks a person’s browser into sending an unwanted request to a site where that person is already logged in.
The important detail is that browsers automatically attach credentials such as session cookies to some requests. Without a separate CSRF check, the server might not be able to tell a request from its own page from a request triggered by an attacker’s page.
A successful attack can change an email address, delete data, make a purchase, or perform any other action available to the victim.
What does a CSRF attack look like?
Imagine this endpoint deletes a car:
https://example.com/api/delete?id=1
If it accepts a GET request, an attacker can put this image on another site:
<img src="https://example.com/api/delete?id=1" alt="" />
The browser requests the URL while trying to load the image. Depending on the cookie settings, it may also send the victim’s session cookie.
Never use GET for an operation that changes data. A GET request should be safe to repeat and should only retrieve data.
Using POST is necessary, but it is not a CSRF defense on its own. An attacker’s page can submit a form:
<form action="https://example.com/api/delete" method="post">
<input type="hidden" name="id" value="1" />
</form>
<script>
document.querySelector('form').submit()
</script>
The same risk applies to PUT, PATCH, and DELETE endpoints when the browser automatically sends authentication credentials.
When is an application vulnerable?
CSRF is mainly a concern when authentication is attached automatically by the browser. Session cookies are the most common example. HTTP Basic authentication and client certificates can have the same property.
An API that only accepts a bearer token in an Authorization header is less exposed to classic CSRF because a random page cannot add that header. You still need to configure CORS correctly and protect the token from XSS.
Use the framework’s CSRF protection
First check whether your framework has a maintained, built-in CSRF feature. It is easy to get the details of token generation and validation wrong.
Do not start a new implementation with the old Express csurf package. Its repository was archived in 2025 and it is no longer maintained.
Use a synchronizer token
For a server-rendered application with sessions, the usual approach is the synchronizer token pattern:
- Generate a secret, unpredictable token on the server.
- Associate it with the user’s session.
- Put it in a hidden form field or send it to your JavaScript application.
- Send it back in the form body or a custom request header.
- Compare it with the session token on the server and reject the request if it is missing or wrong.
Here is the form part:
<form action="/settings/email" method="post">
<input type="email" name="email" />
<input type="hidden" name="csrf_token" value="TOKEN_FROM_SERVER" />
<button>Save</button>
</form>
Do not put a CSRF token in a URL. URLs can appear in browser history, server logs, analytics tools, and Referer headers.
For a single-page application, it is common to send the token in a custom header. If you use the double-submit cookie pattern, use a signed token bound to the current session. Comparing an unsigned cookie with an identical request value is vulnerable to cookie injection in some setups.
Configure SameSite cookies
Set SameSite explicitly on session cookies:
SameSite=Strictsends the cookie only in same-site contexts. It gives stronger protection but can make links from other sites appear to log the user out.SameSite=Laxallows some top-level navigation requests while blocking the cookie on many cross-site subrequests and form submissions.SameSite=Noneallows cross-site use and also requiresSecure.
Also use Secure and HttpOnly for session cookies. Prefer a host-only cookie, or the __Host- cookie prefix when possible, instead of sharing a session cookie with every subdomain.
SameSite is a useful layer, but do not treat it as the only defense for important state-changing operations.
Check where the request came from
The server can reject unsafe requests whose Origin does not match its own origin. When Origin is absent, a carefully implemented check can fall back to Referer.
Modern browsers also send Fetch Metadata headers. In particular, Sec-Fetch-Site: cross-site lets a server identify and reject an obvious cross-site request. Plan a fallback for clients that do not send these headers.
For JSON APIs, requiring a custom request header and rejecting simple form content types adds another barrier. This only works when CORS does not allow untrusted origins to send credentialed requests.
Add confirmation to sensitive actions
For actions such as changing a password or sending money, ask the user to re-enter a password, use a passkey, or confirm with another authentication factor.
This verifies the user’s intent and reduces the damage of several kinds of attacks. It is an extra layer, not a replacement for normal CSRF protection.
Finally, remember that XSS can bypass most CSRF defenses because malicious code running on your own origin can read tokens and send valid requests. You need to prevent both vulnerabilities.
The OWASP CSRF Prevention Cheat Sheet has the detailed implementation guidance.