Cross-Site Request Forgery, usually shortened to CSRF, is one of the oldest and most frequently misunderstood attacks against web applications. The underlying problem sounds harmless enough: whenever your browser sends a request to a domain, it automatically attaches the cookies stored for that domain, even if the request was never triggered by you but by a hostile third-party page. Attackers abuse exactly this automatic behavior to perform actions in your name while you are logged in.
This article explains CSRF from the ground up: how the attack works technically, when an application is actually vulnerable (and when it is not), where CSRF sits in the OWASP Top 10, and how you defend against it with proven patterns and concrete framework code for Spring Security, Django and Node.js. We also draw a clean line between CSRF and two attacks that are routinely confused with it: Cross-Site Scripting (XSS) and Server-Side Request Forgery (SSRF).
By the end you will have not only a solid mental model of the attack, but also a practical checklist and runnable code you can drop into your own projects.
What Is Cross-Site Request Forgery?
The OWASP Foundation defines CSRF as an attack that forces an authenticated end user to execute unwanted actions on a web application they are currently logged into. The attacker never sees the server's response. CSRF therefore targets state-changing actions rather than reading data: change a password, overwrite an email address, transfer money, create a new administrator account.
The attack goes by several names. In the literature you will find the abbreviations CSRF and XSRF, along with the vivid nicknames "Session Riding" and "Sea Surf". They all describe the same idea: the attacker rides on top of the victim's existing, valid session.
Formally, CSRF is classified as CWE-352 (Cross-Site Request Forgery) in MITRE's Common Weakness Enumeration. MITRE describes it as a "composite weakness": the application cannot sufficiently verify whether a request was intentionally provided by the user who sent it, or slipped in by an unauthorized actor. CWE-352 has appeared consistently in the CWE Top 25 most dangerous software weaknesses for years.
How a CSRF Attack Works, Step by Step
The mechanism always relies on the same abuse of trust:
- You log into an application, say your online banking. The server sets a session cookie in your browser.
- Without logging out, you open a page in another tab that an attacker controls (linked via email, chat or a forum post).
- That page contains hidden code that fires a request at the bank's domain.
- Your browser automatically attaches the valid bank session cookie to that request, because it targets the bank's domain.
- The bank's server sees a technically correct, authenticated request and executes it. It cannot distinguish it from a genuine request you triggered yourself.
GET-Based Attack
If a state-changing action is reachable through a plain GET request, an invisible image is enough. The browser loads the "image source" and thereby fires the request:
<img src="http://bank.example/transfer.do?acct=MARIA&amount=100000" width="0" height="0" border="0">
The victim only sees an empty spot on the attacker's page. In the background, the transfer went through. This is precisely why state-changing actions must never be reachable via GET.
POST-Based Attack
Most sensitive actions run over POST. That can be forged too, using a hidden form that JavaScript submits automatically:
<html>
<body>
<form action="https://bank.example/transfer" method="POST">
<input type="hidden" name="acct" value="MARIA" />
<input type="hidden" name="amount" value="100000" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>
As soon as the victim opens the page, the browser submits the form, session cookie included. No interaction whatsoever is required.
A historical example verified against the NVD is CVE-2008-6586 in the WebUI of the BitTorrent client µTorrent 0.315: via CSRF, and without any authentication of the attacker, one could force the download of arbitrary torrent files and even modify the administrator account (CVSS 2.0: 6.8). More recent cases exist as well, such as CVE-2024-12280 in the WordPress plugin "WP Customer Area" (a missing CSRF check on a log-deletion endpoint, per WPScan). That entry, however, is so far only supported by secondary sources.
When Is an Application Actually Vulnerable to CSRF?
This question is decisive and skipped in many explanations. MDN lists three conditions that must all be true at the same time for classic CSRF to be possible:
- The request changes server-side state (not just a read).
- The application identifies the user solely through cookies, with no additional proof such as a custom header or a bearer token.
- The request parameters are fully predictable, so the attacker can construct the request in advance.
The second point clears up the most common confusion among API developers: pure bearer-token APIs are generally not vulnerable to classic CSRF. If the access token travels in the Authorization header (and not in a cookie), the browser does not automatically attach it to a cross-site request. The attacker cannot set that header, and the request stays unauthenticated.
CSRF is therefore fundamentally a problem of cookie- or session-based authentication. Once an application no longer relies on automatically transmitted cookies alone, the attack surface disappears. This only holds as long as the token truly lives only in the header and not also in a cookie. Hybrid models that store tokens in cookies reintroduce the CSRF risk.
> Note: CSRF is not strictly limited to cookie authentication. HTTP Basic and Digest authentication are vulnerable too, because the browser keeps sending the credentials automatically until the session ends.
CSRF in the OWASP Top 10: An Edition-Accurate History
A lot of half-truths circulate about where CSRF sits in the OWASP taxonomy. Here is the documented timeline:
- OWASP Top 10:2013 listed CSRF as its own entry, A8:2013 ("Cross-Site Request Forgery").
- OWASP Top 10:2017 removed CSRF as a standalone item. The rationale: many frameworks now shipped built-in CSRF protection, and CSRF was found in only about 5% of tested applications. The category "Broken Access Control" was also created in 2017.
- OWASP Top 10:2021 lists CSRF explicitly again, though not as its own entry but as a "notable CWE" (CWE-352) within A01: Broken Access Control, the number-one category.
- The OWASP Top 10:2025 edition again explicitly cites CWE-352 under A01: Broken Access Control. What is new there is that Server-Side Request Forgery (CWE-918, SSRF) was folded into the same A01 category.
The key takeaway for correct placement: CSRF is no longer a standalone Top 10 entry today. Since 2021 it has consistently been part of A01 Broken Access Control. Anyone claiming CSRF is still its own item is working from an outdated snapshot.
Our 2013/2017 details rest on consistent secondary sources; the fundamental statement (own entry in 2013, removed in 2017, part of Broken Access Control since) is uncontroversial in the security community. The 2025 edition is now published as a final release (a release candidate appeared in November 2025, finalized in early 2026, and owasp.org/Top10/2025/ presents the edition as final) and again cites CWE-352 under A01 Broken Access Control. For the broader picture, see our deep dive on the OWASP Top 10.
CSRF vs. XSS: Related but Fundamentally Different
CSRF and Cross-Site Scripting (XSS) are often mentioned in the same breath, because both carry "cross-site" in the name and are classic web-application weaknesses tracked in MITRE's CWE catalogue (CSRF as CWE-352, XSS as CWE-79). Technically, though, they are two very different attacks.
The core distinction: XSS injects malicious code directly into a page and executes it in the victim's browser. CSRF injects no code at all. It abuses an existing trust relationship and the automatic transmission of credentials to issue a request on the victim's behalf. Put differently: XSS breaks the browser's trust in the page, while CSRF breaks the server's trust in the authenticated user.
The most important practical link: XSS defeats any CSRF protection. If an attacker can run arbitrary JavaScript in the context of the target site via an XSS flaw, they simply read the CSRF token from the DOM or cookie and send a perfectly valid request. The OWASP Cheat Sheet Series states it plainly: "Cross-Site Scripting (XSS) vulnerabilities can bypass CSRF protections." Rigorous XSS prevention is therefore a prerequisite for any working CSRF defense. For how XSS works in detail and how to stop it, see our XSS basics.
CSRF vs. SSRF: Only the Name Is Similar
Since CSRF and SSRF ended up under the same A01 umbrella in OWASP 2025, they get confused even more often. In substance they have almost nothing in common.
- CSRF (Cross-Site Request Forgery, CWE-352): the attacker makes the victim's browser send a request. What gets exploited is the victim's implicit authentication (the session cookie). Direction: client (victim's browser) to vulnerable server.
- SSRF (Server-Side Request Forgery, CWE-918): the attacker makes the server send a request to a resource of the attacker's choosing, often an internal system that would be unreachable from the outside. Direction: vulnerable server to internal target system.
A quick mnemonic: in CSRF the victim's browser is abused as the tool, in SSRF the server itself. Sharing an OWASP category does nothing to change this fundamentally different attack mechanics.
An Overview of Defenses
There is no single silver bullet, only layered defense. The OWASP Cheat Sheet Series distinguishes two primary token mechanisms and several complementary "defense-in-depth" layers.
Synchronizer Token Pattern (Primary, for Stateful Apps)
The classic and most robust pattern: the server generates a cryptographically unpredictable token per user or session, stores it server-side, and embeds it into every form (as a hidden field or a custom header, never in a cookie alone). On every state-changing request the server compares the submitted token against the stored one. If it is missing or does not match, the request is rejected.
The attacker cannot guess this token, and because of the same-origin policy they cannot read it from the server response either. Important: tokens must never appear in GET URLs, because they can leak there via browser history, log files, the Referer header and network utilities.
Double-Submit Cookie (Primary, for Stateless Apps and APIs)
When the server should hold no server-side state, the double-submit cookie is a good fit: the token is set both as a cookie and additionally sent in a request parameter or header. The server checks that both values match.
One distinction is critical here:
- Naive variant (not recommended): a random cookie value is merely compared against a request value. This is vulnerable to cookie injection, especially when an attacker controls a subdomain or the network environment.
- Signed variant (recommended): the token is cryptographically bound to the session via HMAC. A constant-time comparison additionally protects against timing attacks.
The OWASP Cheat Sheet Series describes the signed double-submit cookie procedure like this:
secret = getSecretSecurely("CSRF_SECRET")
sessionID = session.sessionID
randomValue = cryptographic.randomValue(64)
message = sessionID.length + "!" + sessionID + "!" + randomValue.length + "!" + randomValue.toHex()
hmac = hmac("SHA256", secret, message)
csrfToken = hmac.toHex() + "." + randomValue.toHex()
response.setCookie("csrf_token=" + csrfToken + "; Secure")
Note that this cookie is deliberately not marked HttpOnly: the client-side JavaScript must be able to read the cookie value in order to echo it back as a header or form parameter, which is the whole point of the double-submit pattern. Its security does not rest on HttpOnly but on the same-origin policy plus the HMAC binding to the session.
SameSite Cookie Attribute (Defense-in-Depth)
The SameSite attribute controls whether a cookie is sent on cross-site requests at all. It has three values:
Strict: the cookie is not sent in cross-site contexts. Recommended for highly sensitive session cookies where the user flow allows it (downside: if the user follows an external link to the site, they briefly appear logged out).Lax: the cookie is additionally sent on top-level navigation using safe methods. This is the browser default.None: no protection, the cookie is sent in every context. Strictly requires theSecureflag.
An important fact about the default behavior: if an application sets no SameSite attribute, modern browsers treat the cookie as Lax by default. Chrome introduced this change with version 80 in February 2020; other Chromium-based browsers (Edge, Opera) followed. You will sometimes see the claim "since 2021", which at best refers to fuller enforcement, not the original rollout.
This is how a cookie is delivered with the attribute set:
Set-Cookie: sessionid=7yjgj57e4n3d; SameSite=Strict; Secure; HttpOnly
You must know SameSite's limits: SameSite=Lax does not protect you if a state-changing action is reachable via a GET request. Moreover, SameSite is scoped to the registrable domain, not the origin, which means a compromised subdomain can bypass the protection. SameSite is therefore a valuable extra layer, but never a standalone replacement for a token.
Origin and Referer Checking (Defense-in-Depth)
On state-changing requests the server can verify that the Origin header (or, as a fallback, the Referer) matches its own domain. The Origin header is preferable because it is set only by the browser and is stripped less often by proxies or privacy tools than the Referer. Referer-only checking is considered weaker and should never be the sole defense.
Custom Header for JSON APIs (Defense-in-Depth)
For AJAX/JSON endpoints, the client can set an arbitrary custom header (for example X-CSRF-Token) that the server requires. This leverages the CORS preflight mechanism: a foreign page cannot simply set that header cross-site. One caveat: CORS must never combine Access-Control-Allow-Origin: * with credentials. Browsers reject that combination anyway; the real danger is dynamically reflecting every requesting origin together with credentials.
Fetch Metadata Headers (Modern Addition)
Newer browsers send Sec-Fetch-Site, Sec-Fetch-Mode and related headers that cannot be spoofed. The server can reject an unsafe method coming from a cross-site context. Browser support exceeds 98%, though a fallback to classic header checks remains necessary for older clients.
CSRF Protection in Practice: Framework Code
Theory is grey. Let us look at how leading frameworks defend against CSRF in concrete terms. All snippets come from the official framework documentation and are runnable.
Spring Security (Java)
If you follow our Spring Boot series, the good news is that Spring Security protects against CSRF by default, without a single line of extra code. Protection is active for all unsafe methods (POST, PUT, DELETE). Spelled out, the configuration looks like this:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults());
return http.build();
}
}
By default Spring stores the token in the HttpSession (HttpSessionCsrfTokenRepository). For JavaScript front ends (single-page apps) the cookie-based repository is more suitable; it writes an XSRF-TOKEN cookie and expects the X-XSRF-TOKEN header:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf((csrf) -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
}
Since Spring Security 6 the XorCsrfTokenRequestAttributeHandler is the default. It adds randomness to the transmitted token on each request, which hardens against BREACH attacks (a compression side channel). In a classic form, Thymeleaf or the Spring form tag library embeds the token automatically; done manually, the hidden field looks like this:
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
For stateless REST APIs that authenticate exclusively via bearer token, CSRF protection is unnecessary and may be disabled deliberately. The recommendation is to exempt only the affected paths rather than switching it off globally:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf((csrf) -> csrf
.ignoringRequestMatchers("/api/**")
);
return http.build();
}
}
And because security without tests is worthless: Spring Security provides MockMvc extensions that let you assert both the success case (with(csrf())) and the correct rejection of an invalid token (with(csrf().useInvalidToken())).
@Test
public void loginWhenInvalidCsrfTokenThenForbidden() throws Exception {
this.mockMvc.perform(post("/login").with(csrf().useInvalidToken())
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().isForbidden());
}
Django (Python)
Django ships CSRF protection framework-wide through CsrfViewMiddleware, enabled by default:
MIDDLEWARE = [
"django.middleware.csrf.CsrfViewMiddleware",
# ... other middleware
]
In every internal POST form you add the template tag {% csrf_token %}. Django then renders a hidden field and validates it server-side:
<form method="post">{% csrf_token %}
<!-- form fields -->
</form>
Caution: never use {% csrf_token %} in forms that submit to an external URL, or you leak your token to a third party.
For AJAX calls, read the token from the csrftoken cookie and send it in the custom header X-CSRFToken. The mode: 'same-origin' parameter ensures the token never reaches a foreign domain:
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + "=")) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie("csrftoken");
const request = new Request("/api/update", {
method: "POST",
headers: { "X-CSRFToken": csrftoken },
mode: "same-origin"
});
fetch(request).then((response) => {
// process the response
});
For individual views Django offers fine-grained decorators: csrf_protect (force protection), csrf_exempt (deliberately disable it), ensure_csrf_cookie (guarantee the cookie is set, useful for pure AJAX pages). One important testing note: Django's test client relaxes CSRF checks by default. If you want to test enforcement itself, instantiate it explicitly with Client(enforce_csrf_checks=True).
Node.js / Express: Beware of csurf
Many older Express tutorials reach for the csurf package. That advice is out of date: the official csurf repository (expressjs/csurf) is archived and no longer maintained. There are no more security updates and no more bug fixes. Here is how csurf was classically used (now obsolete, shown only so you recognize it):
// OBSOLETE: expressjs/csurf is archived and no longer maintained
var cookieParser = require('cookie-parser')
var csrf = require('csurf')
var express = require('express')
var csrfProtection = csrf({ cookie: true })
var app = express()
app.use(cookieParser())
app.get('/form', csrfProtection, function (req, res) {
res.render('send', { csrfToken: req.csrfToken() })
})
Current recommendation: use a maintained library such as csrf-csrf (implements the double-submit cookie pattern for stateless apps) or csrf-sync (Synchronizer Token Pattern for session-based apps). Check each package's maintenance status on npm before adopting it. Alternatively, implement the protection yourself in a few lines. The following example shows the double-submit pattern with a constant-time comparison:
const express = require('express');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const app = express();
app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));
// Set the token when serving the form
app.get('/form', (req, res) => {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrf_token', token, { sameSite: 'strict', secure: true });
res.send(`<form method="POST" action="/process">
<input type="hidden" name="_csrf" value="${token}">
<button type="submit">Submit</button>
</form>`);
});
// Double-submit check as middleware
function verifyCsrf(req, res, next) {
const cookieToken = req.cookies.csrf_token;
const bodyToken = req.body._csrf;
if (!cookieToken || !bodyToken ||
cookieToken.length !== bodyToken.length ||
!crypto.timingSafeEqual(Buffer.from(cookieToken), Buffer.from(bodyToken))) {
return res.status(403).send('CSRF validation failed');
}
next();
}
app.post('/process', verifyCsrf, (req, res) => {
res.send('request processed');
});
For production, extend this baseline with the HMAC-signed variant (shown above) or a vetted library, since the naive double-submit check on its own is vulnerable to cookie injection.
Other Frameworks in Brief
- ASP.NET MVC: uses the anti-forgery token scheme, a double token made of a cookie and a hidden form field (
@Html.AntiForgeryToken()). Microsoft explicitly warns that SSL/HTTPS alone does not prevent CSRF, since the attacker's page can sendhttps://requests too. - Flask: via the Flask-WTF extension, enabled with
CSRFProtect(app). - Angular: has built-in XSRF support through
withXsrfConfiguration, which reads theXSRF-TOKENcookie and sends it as theX-XSRF-TOKENheader. On a related note, our article on XSS protection in React covers adjacent client-side security concerns.
Common Implementation Mistakes in the Wild
A token alone is useless if it is validated incorrectly. The PortSwigger Web Security Academy documents several recurring mistakes that real applications fall for:
- The token is only checked on POST, not on GET. If the same action is reachable via GET too, the attacker bypasses the check.
- A missing token is ignored instead of rejected. Some applications only validate the token when it is present. Omit it entirely and the request goes through.
- The token is not tied to the session. If any valid token is accepted, the attacker can grab one of their own and use it.
- SameSite=Lax is bypassed via a state-changing GET or an "on-site gadget".
- Referer checking is defeated by dropping or spoofing the header.
Vendor sources such as Wiz also name the classic misconception that HTTPS alone protects against CSRF. That is wrong: TLS encrypts the transport but does not distinguish a genuine request from a forged one.
CSRF Cookies and Privacy Law
Worth a brief look, especially for readers subject to EU privacy law, is a dimension missing from almost every technical explanation. This is explicitly not legal advice, only context. Under EU privacy rules (in Germany specifically § 25 TDDDG, the successor to the TTDSG, which implements the ePrivacy Directive), cookies generally require consent unless they are "strictly necessary" for the service.
A CSRF cookie that secures a logged-in session will in many cases qualify as strictly necessary. It becomes questionable when a CSRF cookie is set indiscriminately for all visitors, including unauthenticated ones for whom no session worth protecting exists at all. In such setups, classifying the cookie as "necessary" is doubtful. Because functionally equivalent alternatives without a persistent cookie exist (token as a POST parameter, SameSite attributes, server-side origin checks), the cookie can often be limited to genuinely authenticated contexts. When in doubt, have the concrete legal assessment reviewed by a qualified professional.
Frequently Asked Questions
What is Cross-Site Request Forgery (CSRF)?
CSRF is an attack that tricks a logged-in user into unknowingly performing a state-changing action on a web application they are currently authenticated to. It exploits the fact that the browser automatically attaches stored session cookies to any request aimed at the target domain, even when the request originates from a hostile third-party page. The attacker never sees the server's response; they only care about the action that fires, such as a money transfer or a password change.
What is the difference between CSRF and XSS?
XSS (Cross-Site Scripting) injects malicious code into a page and runs it in the victim's browser. CSRF injects no code at all; it abuses an existing trust relationship to send a request on the victim's behalf. Crucially, XSS defeats any CSRF protection, because through an XSS flaw an attacker can read the CSRF token directly from the DOM or cookie. Rigorous XSS prevention is therefore a prerequisite for CSRF defense to work at all.
How do you prevent CSRF attacks?
The most effective defense is a CSRF token: the Synchronizer Token Pattern for stateful applications, and a signed double-submit cookie or a mandatory custom header for stateless apps and APIs. The SameSite cookie attribute and a server-side origin check add valuable extra layers. State-changing actions must also never be reachable via GET. Modern frameworks such as Spring Security and Django ship this protection by default.
Are REST APIs vulnerable to CSRF?
Pure bearer-token APIs are generally not vulnerable to classic CSRF. If the access token travels in the Authorization header rather than a cookie, the browser does not attach it automatically to a cross-site request, and the attacker cannot set that header. This only holds while the token lives exclusively in the header, though. Hybrid models that also store tokens in cookies reintroduce the CSRF risk.
Does the SameSite cookie attribute fully prevent CSRF?
No. SameSite (especially Strict) is a valuable defense-in-depth layer, but not a full replacement for a token. SameSite=Lax, the browser default, does not protect you when a state-changing action is reachable via a GET request. SameSite is also scoped to the registrable domain rather than the origin, so a compromised subdomain can bypass it. Always pair SameSite with a token.
Conclusion and Checklist
CSRF is not an exotic corner case but a structural attack on the very mechanics of cookie-based sessions. The good news: modern frameworks ship effective protection, often on by default. The bad news: a single misconfiguration, one state-changing GET endpoint, or an open XSS hole is enough to undo it.
Your practical checklist:
- Never make state-changing actions reachable via GET.
- For stateful applications, use the Synchronizer Token Pattern (leave the framework's protection enabled).
- For stateless apps and APIs, use the signed double-submit cookie or a mandatory custom header.
- Add SameSite (preferably
Strictfor sensitive session cookies) and origin checking as extra layers. - Pure bearer-token APIs are not vulnerable to classic CSRF; disable CSRF protection there deliberately and selectively.
- Prevent XSS rigorously, because XSS defeats any CSRF protection.
- Do not use abandoned libraries:
csurfis archived, move to a maintained alternative. - Place CSRF correctly: since 2021 it is part of A01 Broken Access Control in the OWASP Top 10, no longer a standalone entry.
Follow these points and you leave classic CSRF practically no room to operate.