Published
- 30 min read
How to Avoid Cross-Site Request Forgery (CSRF)
How to Write, Ship, and Maintain Code Without Shipping Vulnerabilities
A hands-on security guide for developers and IT professionals who ship real software. Build, deploy, and maintain secure systems without slowing down or drowning in theory.
Buy the book now
Practical Digital Survival for Whistleblowers, Journalists, and Activists
A practical guide to digital anonymity for people who can’t afford to be identified. Designed for whistleblowers, journalists, and activists operating under real-world risk.
Buy the book now
The Digital Fortress: How to Stay Safe Online
A simple, no-jargon guide to protecting your digital life from everyday threats. Learn how to secure your accounts, devices, and privacy with practical steps anyone can follow.
Buy the book nowIntroduction
Cross-Site Request Forgery (CSRF) is a malicious attack that tricks users into performing unauthorized actions on a web application in which they are authenticated. By exploiting the trust a server has in the user’s browser, attackers can manipulate requests to perform actions such as changing account settings, initiating financial transactions, or compromising sensitive data.
Despite being a well-documented threat, CSRF remains a common vulnerability in web applications. This article provides a detailed overview of CSRF, its potential impacts, and practical techniques to secure your web applications against these attacks.
What is Cross-Site Request Forgery (CSRF)?
CSRF occurs when an attacker tricks a user into unknowingly submitting a request to a web application where they are authenticated. Since the user’s session cookies are automatically included with the request, the server processes it as legitimate, even though the user did not intend to perform the action.
How CSRF Works
- Authentication Context:
- The user logs into a web application, and the browser retains session cookies.
- Malicious Request:
- The attacker creates a malicious link or form that initiates a request to the target application.
- Trick User into Submission:
- The user unknowingly triggers the request by clicking the link or submitting the form.
- Server Processes Request:
- The server, relying on the session cookies, treats the request as originating from the authenticated user.
Example of a CSRF Attack:
A user is logged into their banking application. An attacker sends them an email containing a link:
<img src="https://banking-app.com/transfer?amount=1000&to=attacker_account" />
When the user views the email, the request is sent to the banking application, transferring money without the user’s knowledge.
CSRF Attack Mechanics in Depth
Understanding how a CSRF exploit is constructed makes it far easier to defend against one. At its core, a CSRF attack exploits a fundamental property of the web: browsers automatically send cookies with every request to a domain, regardless of which page initiated that request.
The Anatomy of a GET-Based Attack
The simplest variant targets endpoints that change state via HTTP GET. Consider a banking application that processes fund transfers with a URL like:
GET https://bank.example.com/transfer?to=bob&amount=500 HTTP/1.1
Cookie: session=a1b2c3d4e5f6
An attacker who knows this URL structure can embed it in a zero-pixel image tag inside a phishing email or a malicious web page:
<!-- Invisible to the victim—fetched automatically on page load -->
<img src="https://bank.example.com/transfer?to=attacker&amount=50000" width="0" height="0" alt="" />
When a logged-in victim loads the email or page, the browser silently fires the GET request and the session cookie rides along—no click required.
The POST-Based Attack
Many developers assume that switching to POST prevents CSRF. It does not. An attacker can host an auto-submitting HTML form that fires the moment a victim opens the attacker’s page:
<form id="csrfForm" action="https://bank.example.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker_account" />
<input type="hidden" name="amount" value="50000" />
</form>
<script>
document.getElementById('csrfForm').submit()
</script>
The victim sees a blank page for a split second, but the transfer has already been submitted with their authenticated session cookie attached by the browser.
Attack Flow Diagram
sequenceDiagram
actor Victim
participant Browser
participant AttackerSite as Attacker's Page
participant BankApp as Bank Application
Victim->>Browser: Logs into Bank App
BankApp-->>Browser: Set-Cookie: session=abc123; HttpOnly
Note over Victim,Browser: Days later...
Victim->>Browser: Opens phishing email link
Browser->>AttackerSite: GET /exploit.html
AttackerSite-->>Browser: Auto-submit form targeting bank
Browser->>BankApp: POST /transfer (cookie auto-attached)
Note right of Browser: session=abc123 included automatically
BankApp-->>Browser: HTTP 200 — Transfer complete
Note over BankApp: Attacker receives funds
Stored CSRF
A particularly dangerous variant is stored CSRF, where the attacker plants the malicious payload directly inside the vulnerable application—for example, in a profile bio field that renders unescaped HTML. Any logged-in user who views the infected profile automatically fires the forged request. Because the payload lives on a trusted domain, defenses like Referer header checking are bypassed entirely. Stored CSRF amplifies the attack’s reach dramatically: a single planted payload can affect every authenticated user who visits the page.
What CSRF Cannot Do
It is equally important to understand CSRF’s limitations. Because the attack is a blind, fire-and-forget technique, the attacker’s page cannot read the server’s response. CSRF is suitable for executing actions—transfers, password resets, account deletions—but cannot directly exfiltrate data unless combined with a separate XSS vulnerability. This distinction matters for threat modeling: CSRF is about causing unauthorized state changes, not data theft.
CSRF also does not bypass server-side authorization logic. If a user’s role does not permit them to perform an action, a forged request submitted in their name will still fail—because the request runs with the victim’s privileges, not elevated ones. However, this provides no comfort if the victim is an administrator: in that case, CSRF can be used to create new admin accounts, delete content, or change system-wide configuration on the attacker’s behalf.
Finally, HTTPS alone does not prevent CSRF. The encrypted channel protects the request contents from eavesdroppers in transit, but the browser still sends cookies automatically on an HTTPS cross-site request, so the attack proceeds exactly as it would over HTTP.
The Impact of CSRF Attacks
CSRF attacks can have severe consequences, including:
- Unauthorized Transactions:
- Transferring money, purchasing items, or making changes to sensitive account settings.
- Data Breaches:
- Manipulating API requests to extract sensitive information.
- Privilege Escalation:
- Exploiting administrative privileges to compromise system security.
- Reputational Damage:
- Loss of user trust in applications that fail to protect against such attacks.
- Regulatory Violations:
- Non-compliance with security standards like GDPR or PCI DSS due to insufficient protections.
To put these risks in concrete terms: a CSRF vulnerability in a payment processing application could allow an attacker to initiate transactions on behalf of every logged-in customer simultaneously—the scale limited only by how many users can be reached via the attack vector. In 2008, a CSRF flaw in the uTorrent client was exploited at mass scale to force users’ BitTorrent clients to download malware by embedding the attack URL in online advertisements. Closer to the present, CSRF vulnerabilities have been found in enterprise SaaS platforms, content management systems, and banking portals, often resulting in multi-million dollar fraud losses or mandatory regulatory disclosures.
A 2024 analysis of vulnerability disclosures showed that CSRF remains consistently present in the OWASP Top 10 candidates for API security, particularly because REST APIs are sometimes deployed without the CSRF protections that were standard in server-rendered applications. Every new architectural paradigm—microservices, SPAs, serverless functions—creates new surfaces for CSRF unless developers explicitly carry protection practices forward.
Techniques to Prevent CSRF
Preventing CSRF involves implementing mechanisms that verify the authenticity of user requests. Here are the most effective techniques:
1. Implement CSRF Tokens
CSRF tokens are unique, random values generated by the server and included in forms or requests. The server validates these tokens to ensure the request originated from a legitimate source.
How CSRF Tokens Work:
- The server generates a token and embeds it in the HTML form as a hidden field.
- When the form is submitted, the token is sent along with the request.
- The server verifies the token against a stored value before processing the request.
Example (Django):
Django automatically includes CSRF protection:
<form method="POST">
{% csrf_token %}
<input type="text" name="example_field" />
<button type="submit">Submit</button>
</form>
Node.js / Express
The csrf-csrf package provides a modern, maintained double-submit implementation for Express:
import { doubleCsrf } from 'csrf-csrf'
const { doubleCsrfProtection, generateToken } = doubleCsrf({
getSecret: () => process.env.CSRF_SECRET,
cookieName: '__Host-psifi.x-csrf-token',
cookieOptions: { sameSite: 'lax', secure: true }
})
app.use(doubleCsrfProtection)
app.get('/form', (req, res) => {
const csrfToken = generateToken(req, res)
res.render('form', { csrfToken })
})
app.post('/submit', (req, res) => {
// Middleware has already validated the token; safe to proceed
res.json({ status: 'ok' })
})
Pass csrfToken to your template and embed it as a hidden field or read it in JavaScript from the token cookie for AJAX calls.
PHP
In a plain PHP application, generate a cryptographically random token, store it in the session, and compare on submission using a constant-time equality check:
<?php
session_start();
// Token generation — once per session
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrfToken = $_SESSION['csrf_token'];
?>
<form method="POST" action="/update-email">
<input type="hidden" name="csrf_token"
value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8') ?>">
<input type="email" name="email" />
<button type="submit">Update</button>
</form>
Server-side validation:
<?php
session_start();
$submitted = $_POST['csrf_token'] ?? '';
$expected = $_SESSION['csrf_token'] ?? '';
if (!hash_equals($expected, $submitted)) {
http_response_code(403);
exit('CSRF token mismatch');
}
// Safe to process the request
hash_equals performs a constant-time comparison, preventing timing-based token-guessing attacks.
CSRF Token Best Practices Across All Frameworks
Regardless of the language or framework you use, the following rules apply universally to CSRF token implementation:
Token entropy: Generate tokens using a cryptographically secure pseudo-random number generator (CSPRNG) with at least 128 bits of entropy—preferably 256 bits. Tokens shorter than 128 bits can theoretically be brute-forced given enough requests.
Token storage on the server: For synchronizer tokens, store the token in the server-side session, not in the database or a shared cache unless that cache is both trusted and protected from inspection. Never regenerate a token on every request—this breaks the back button and multi-tab usage without adding meaningful security.
Token freshness: Issue a new CSRF token after every successful login and after privilege escalation events (e.g., re-authentication). If the session is regenerated, the token must be regenerated too.
Token scope: Bind the token to the authenticated user’s session. A token valid for User A must not be accepted for User B’s session, even if both tokens are individually valid. This prevents token-sharing attacks in shared environments.
Token transmission: Never include the CSRF token as a query parameter in URLs. Always transmit it in the request body (for form submissions) or in a custom request header (for AJAX/API calls). Both mechanisms are inaccessible to cross-site attackers due to the same-origin policy.
Token logging: Never log CSRF tokens in application logs, web server access logs, or distributed tracing systems. Treat a CSRF token with the same sensitivity as a session ID—it is a short-lived authentication artifact.
Spring Boot (Java)
Spring Security enables CSRF protection by default for stateful (session-based) applications. Configure the token repository to allow JavaScript access for AJAX-heavy apps:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
return http.build();
}
}
In Thymeleaf templates, the th:action tag helper injects the token automatically:
<form th:action="@{/transfer}" method="post">
<input type="number" name="amount" />
<button type="submit">Transfer</button>
</form>
ASP.NET Core
ASP.NET Core’s anti-forgery system is built into Razor Pages and MVC. Form tag helpers inject the token automatically:
<form method="post">
<input asp-for="Email" />
<button type="submit">Save</button>
</form>
For controller actions, validate with the [ValidateAntiForgeryToken] attribute:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult UpdateEmail(UpdateEmailModel model)
{
// Token validated by the attribute — safe to proceed
return Ok();
}
For fetch/AJAX requests, read the RequestVerificationToken from the hidden input and send it as the RequestVerificationToken request header.
2. Use SameSite Cookies
The SameSite attribute restricts cookies from being sent with cross-site requests, mitigating the risk of CSRF.
Example:
Set-Cookie: sessionid=abc123; SameSite=Strict
- Strict: Cookies are not sent with any cross-site requests.
- Lax: Cookies are sent with top-level navigation but not with embedded content (e.g.,
<img>or<iframe>).
3. Require User Authentication for Sensitive Actions
Implement authentication checks for critical operations. For example:
- Require users to re-enter their password or use two-factor authentication (2FA) before transferring funds.
4. Verify HTTP Referrer Header
The Referrer header indicates the origin of a request. Servers can verify this header to ensure requests come from trusted sources.
Limitations:
- Some browsers may omit the
Referrerheader due to privacy settings.
5. Avoid Using GET for State-Changing Operations
State-changing actions (e.g., updating user settings or deleting records) should be performed using POST or PUT requests instead of GET. GET requests are more susceptible to CSRF because they can be triggered by simple links or image tags.
SameSite Cookie Protection: A Deep Dive
The SameSite cookie attribute is one of the most powerful modern defenses against CSRF because it instructs browsers not to attach cookies to cross-site requests at all. However, its nuances matter enormously in practice, and misunderstanding them leads to a false sense of security.
The Three Values Compared
| Value | Behaviour | CSRF Protection Level |
|---|---|---|
Strict | Cookie sent only for same-site requests. Never sent on cross-site navigations, even top-level. | Strongest — prevents all cross-site cookie transmission. |
Lax | Cookie sent for safe top-level navigations (clicking a link) but blocked for embedded resources and cross-site POSTs. | Good — blocks the most common CSRF vectors. |
None | Cookie sent with all requests, including cross-site. Requires Secure. | No protection. |
Set-Cookie: sessionid=abc123; SameSite=Strict; Secure; HttpOnly; Path=/
Set-Cookie: sessionid=abc123; SameSite=Lax; Secure; HttpOnly; Path=/
Choosing the Right Level
SameSite=Strict is ideal for high-privilege cookies such as admin or banking session tokens. The trade-off is usability: if a user clicks a link to your application from an email client or a third-party site, their session cookie will not be included and they will land on the logged-out view. For applications where this slight friction is acceptable, Strict provides the strongest guarantee.
SameSite=Lax has been the browser default in Chrome since 2020, and Firefox and Edge have followed suit. It blocks forged POST requests—the most exploited CSRF vector—while still allowing the session cookie to be sent when a user navigates to your site by clicking a link. This makes it the practical choice for most consumer-facing web applications.
The Chrome 2-Minute Lax Window
Chrome introduced a compatibility behaviour: cookies that have no explicit SameSite attribute are treated as Lax, except for the first 120 seconds after the cookie is set, during which they are also sent on cross-site POST requests. This window was added to accommodate legacy single sign-on systems that relied on cross-site form POSTs during login. Always set SameSite explicitly to eliminate this window entirely.
Subdomain Pitfalls and the __Host- Prefix
SameSite considers two URLs to be “same-site” if they share the same registrable domain (eTLD+1), regardless of subdomain. A cookie for app.example.com is visited same-site from evil.example.com. If an attacker can compromise or inject content on any subdomain of your domain—through a DNS takeover of a decommissioned cloud resource, for example—they can initiate requests that are treated as same-site and bypass SameSite protection.
The __Host- cookie name prefix closes this loophole and should be used whenever possible:
Set-Cookie: __Host-session=abc123; SameSite=Strict; Secure; HttpOnly; Path=/
A cookie with the __Host- prefix:
- Must carry the
Secureflag (HTTPS only). - Must not include a
Domainattribute, preventing it from being scoped to subdomains. - Must have
Path=/.
This makes it cryptographically impossible for a sibling subdomain to overwrite or shadow the cookie.
SameSite Is Defense-in-Depth, Not a Silver Bullet
SameSite is an excellent secondary layer, but it should not replace CSRF tokens in the primary defense. A small percentage of traffic still comes from legacy browsers or non-standard user agents that do not enforce SameSite. Additionally, SameSite=Lax still permits cross-site top-level GET navigations—if your application uses GET for any state-changing operation, those endpoints remain exposed. Always combine SameSite with synchronizer tokens or signed double-submit cookies.
The Double-Submit Cookie Pattern
For stateless applications, microservices, or REST APIs where maintaining server-side session state to store CSRF tokens is impractical, the double-submit cookie pattern provides a scalable and stateless alternative. The core idea is to place an identical CSRF token in both a cookie and a request parameter (either a custom header or a form field), and verify on the server that the two values match.
How It Works
Step 1 — The server sets a non-HttpOnly CSRF token cookie when the user loads the application:
Set-Cookie: csrf_token=<random_value>; SameSite=Strict; Secure; Path=/
Note the absence of HttpOnly — JavaScript must be able to read this cookie.
Step 2 — Client-side JavaScript reads the cookie value and attaches it to every state-changing request as a custom header:
function getCookie(name) {
const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'))
return match ? decodeURIComponent(match[1]) : null
}
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('csrf_token') // double-submit
},
body: JSON.stringify({ to: 'bob', amount: 100 })
})
Step 3 — The server verifies that the value in the X-CSRF-Token header matches the value in the csrf_token cookie. Mismatch → reject with HTTP 403.
Why This Stops CSRF
An attacker’s cross-site page can cause the browser to include the csrf_token cookie automatically, but it cannot read that cookie’s value due to the browser’s same-origin policy. Because the attacker cannot read the value, it cannot embed a matching X-CSRF-Token header in the forged request, and the server rejects it.
The Naive Variant and Its Weakness
The basic double-submit pattern has a known vulnerability: cookie injection. If an attacker controls a subdomain of your domain—through a DNS takeover of an expired cloud resource—they can plant a csrf_token cookie with a value they know, then simultaneously supply that same value in the forged request header, bypassing the check entirely.
The Signed (HMAC) Variant — Recommended
The recommended mitigation is to sign the token with a server-side secret using HMAC-SHA256, binding the token to the user’s session:
csrfToken = HMAC-SHA256(secret, sessionId + "!" + randomNonce) + "." + randomNonce
Validation:
- Split the token into its HMAC component and the random nonce.
- Recompute
HMAC-SHA256(secret, currentSessionId + "!" + nonce). - Compare the recomputed HMAC to the received HMAC using a constant-time comparison.
Because the HMAC is keyed to the server’s secret and bound to the active session ID, a subdomain-injected cookie cannot produce a valid HMAC—the check fails and the request is rejected.
When to Use Which Pattern
| Scenario | Recommended Pattern |
|---|---|
| Server-side sessions (PHP, Django, Rails) | Synchronizer Token (server stores and compares) |
| Stateless REST API with JWT cookies | Signed Double-Submit Cookie (HMAC-bound) |
| Distributed microservices | Signed Double-Submit Cookie |
| SPA with a dedicated JSON API | Custom request header (X-CSRF-Token) |
Common Mistakes and Anti-Patterns
Even teams that are well aware of CSRF often introduce subtle vulnerabilities during implementation. Recognising these common pitfalls is as important as knowing the correct techniques.
Storing the Token in a Cookie Only and Comparing Cookie to Itself
Setting the token in a cookie and comparing the cookie value to the same cookie at validation time provides zero protection. The browser sends cookies automatically, so an attacker’s forged request carries the cookie too. The token in the cookie must always be compared to a value submitted in a request header or form body—never to itself.
Using Predictable or Static Token Values
A CSRF token is effective only if it is impossible for an attacker to guess. Deriving tokens from static, user-specific data—such as the user’s ID, email address, or an incrementing integer—is insecure. Always generate tokens with a cryptographically secure random number generator:
import secrets
# 256 bits of randomness
token = secrets.token_hex(32)
// Node.js
const crypto = require('crypto')
const token = crypto.randomBytes(32).toString('hex')
Including the Token in the URL
Never include CSRF tokens in query strings or URL paths. URLs appear in:
- Browser history
- Server access logs
- HTTP
Refererheaders forwarded to third-party scripts on the page - Caches and proxy logs
Always transmit tokens in POST body fields or custom request headers.
Skipping Constant-Time Comparison
Comparing tokens with a simple equality operator (token == expected) can leak information through timing side-channels. Use constant-time comparison functions:
import hmac
if not hmac.compare_digest(submitted_token, expected_token):
abort(403)
// Node.js — crypto.timingSafeEqual requires equal-length Buffers
const crypto = require('crypto')
const a = Buffer.from(submitted, 'utf8')
const b = Buffer.from(expected, 'utf8')
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(403).send('Forbidden')
}
Assuming JSON-Only APIs Are Immune
Developers sometimes believe that JSON APIs are CSRF-safe because browsers cannot set Content-Type: application/json on a cross-origin form POST. While there is truth to this, it is not a reliable defense. Some frameworks accept text/plain bodies that can be sent cross-site; browser behaviour around content types can change; and future API changes can silently remove the protection. Always apply explicit CSRF protection to state-changing endpoints.
Disabling Framework-Level CSRF Protection Globally
Globally disabling CSRF protection—often done to fix a test failure or integrate a third-party webhook—is one of the most dangerous maintenance mistakes in web security. If you must exempt specific endpoints (e.g., public webhooks), do so at the route level and protect those endpoints with alternative mechanisms such as HMAC signature verification of the request body rather than removing protection wholesale.
Testing for CSRF Vulnerabilities
Regular testing is the only reliable way to confirm that CSRF protections are in place and correctly implemented across every state-changing endpoint. Testing should cover new features as they are developed, existing endpoints after architectural changes, and the application as a whole on a periodic schedule. Both manual techniques and automated tools are necessary: automated scanners are efficient at broad coverage, while manual testing is required to understand complex flows such as multi-step transactions, login forms, and authenticated API calls.
Manual Testing\n\nManual testing gives you the deepest insight into how your application validates (or fails to validate) CSRF protections. The goal is not just to confirm that a token field exists, but to verify that the server actively rejects requests when the token is absent, tampered, or replayed across sessions.\n\nEffective manual steps:\n\n- Intercept a state-changing POST request using a browser proxy (Burp Suite or OWASP ZAP).\n- Remove the CSRF token field or header entirely and forward the request. A properly protected endpoint must return a 4xx error code, not process the action.\n- Change the token value to an arbitrary string. The server should reject it with the same 4xx response.\n- Copy a valid CSRF token from one authenticated browser session and submit it in a second authenticated session (different user). The server must reject the cross-session token.\n- Use browser developer tools to modify the SameSite attribute of the session cookie to None and attempt a cross-origin form submission to simulate an older browser.
Automated Tools
- OWASP ZAP: Identifies CSRF vulnerabilities in web applications.
- Burp Suite: Provides detailed insights into CSRF risks and offers tools to test token validation.
Testing with Burp Suite: Step-by-Step
Burp Suite’s built-in CSRF PoC generator makes manual verification fast and repeatable:
- Set Burp as an intercepting proxy and log into the target application.
- Intercept a state-changing request (e.g., a profile update POST) in the Proxy tab.
- Right-click the request → Engagement Tools → Generate CSRF PoC. Burp produces a self-contained HTML file that replicates the request with all parameters.
- Open the PoC file in a browser tab where you are already logged in to the target application. If the action succeeds (profile is updated, funds transferred), the endpoint is vulnerable.
- To verify token validation is enforced: edit the generated PoC to tamper with the CSRF token value. A properly protected server must respond with HTTP 403 or 422 — any 2xx response indicates broken validation.
Testing with OWASP ZAP
ZAP’s active scanner includes a dedicated CSRF detection rule. To focus on CSRF specifically:
- Configure ZAP as a proxy and browse the authenticated portions of the application to populate the Sites tree.
- Right-click a state-changing POST request in the Sites tree → Active Scan.
- After scanning, inspect the Alerts panel for
Anti-CSRF Tokensfindings. ZAP flags endpoints that respond successfully to requests without a valid token.
Automated CSRF Testing Checklist
A thorough manual and automated CSRF test should verify each of the following:
| Test Case | Expected Result |
|---|---|
| Submit a state-changing POST without any CSRF token | Server rejects with 4xx |
| Submit with an incorrect or tampered CSRF token | Server rejects with 4xx |
| Replay a valid CSRF token from a different user’s session | Server rejects with 4xx |
Submit with Origin: https://evil.example.com header | Server rejects or logs anomaly |
Submit with the Referer header removed | Server behaviour documented; token validation still enforced |
Remove the SameSite attribute in DevTools and submit cross-site | Server rejects via token check |
Integrating CSRF Checks into CI/CD
Detecting CSRF regressions early is far cheaper than discovering them in production or via a penetration test. Consider adding:
- OWASP ZAP baseline scan – runs as a Docker container inside your CI pipeline and reports CSRF-related findings automatically on each build.
- Nuclei – the open source vulnerability scanner ships with CSRF detection templates that can be executed as
nuclei -t vulnerabilities/csrf -u https://staging.example.com. - Custom integration tests – write tests that deliberately omit or corrupt the CSRF token in a POST request and assert a
403response. These tests are lightweight, fast, and catch regressions the moment a developer accidentally disables protection.
Testing Login CSRF
Login forms are frequently overlooked in CSRF assessments because the victim is unauthenticated at the time of the attack. Login CSRF is nevertheless a genuine and distinct threat: the attacker tricks the victim’s browser into submitting a login form using the attacker’s own credentials. If the victim does not notice, they continue using the application under the attacker’s account and may enter sensitive data—credit card numbers, personal details, health information—that the attacker can then harvest by logging back in.
To test for login CSRF:
- Confirm whether the login form includes a CSRF token at all (many do not).
- Craft a cross-origin auto-submitting form that posts the login endpoint with valid attacker credentials.
- Open the form in a browser session that has no prior authentication cookies for the target site.
- If the victim is silently logged in as the attacker, the login endpoint is vulnerable.
Mitigating login CSRF requires creating a pre-session: issue a temporary, unauthenticated session cookie when the login page is first loaded, embed a CSRF token tied to that pre-session, and validate the token on the login POST. After successful authentication, destroy the pre-session entirely and create a new authenticated session to prevent session fixation.
Testing for Stored CSRF
Stored CSRF is harder to detect with automated scanners because the payload is injected into the application’s own database. When testing manually:
- Identify input fields that accept and later render HTML (bios, comments, custom fields).
- Submit a payload such as
<img src="/account/delete" width="0" height="0">(using your own test account so no harm is done). - Log in as a different user and view the page that renders the stored content.
- Check whether the state-changing request fired automatically.
This test is best performed in a dedicated test environment to avoid accidental impact on real users.
Example of Secure Implementation
Let’s walk through a secure implementation of CSRF protection in a hypothetical application:
Step 1: Generate and Include CSRF Tokens
In a form:
<form method="POST" action="/update-profile">
<input type="hidden" name="csrf_token" value="random_unique_token" />
<input type="text" name="username" value="current_username" />
<button type="submit">Update</button>
</form>
Step 2: Verify Tokens on the Server
In Python:
from flask import request, session
@app.route('/update-profile', methods=['POST'])
def update_profile():
token = request.form.get('csrf_token')
if not token or token != session['csrf_token']:
return "Invalid CSRF token", 403
# Process the request
return "Profile updated successfully"
Step 3: Use Secure Cookies
Set cookies with the HttpOnly and SameSite attributes to prevent unauthorized access and reduce cross-site risks:
Set-Cookie: sessionid=xyz456; HttpOnly; Secure; SameSite=Strict
Building a CSRF-Resilient Application
Beyond implementing specific measures, fostering a culture of security within your development team is essential. Encourage practices such as:
- Conducting regular code reviews with a focus on CSRF risks.
- Training developers on secure coding practices and the importance of preventing CSRF.
- Integrating automated security scans into CI/CD pipelines.
Defense-in-Depth: Layering Multiple Controls
The most resilient CSRF defenses are composed of multiple independent layers. If one technique is bypassed—whether due to a browser quirk, a legacy client, or a configuration error—the other layers still hold. The following table shows how complementary techniques combine to create robust protection:
| Layer | Technique | What It Covers |
|---|---|---|
| 1 | Synchronizer tokens or signed double-submit cookies | Server-side request authenticity |
| 2 | SameSite=Strict or Lax on all session cookies | Browser-level cross-site request restriction |
| 3 | Origin / Referer header verification | Secondary server-side origin check |
| 4 | Re-authentication for critical actions (password changes, payments) | High-privilege state changes |
| 5 | Automated CSRF scanning in CI/CD pipelines | Regression prevention |
Security Headers That Complement CSRF Protection
Several HTTP response headers actively reduce the risk and impact of CSRF attacks by limiting what a forged request can trigger.
Content-Security-Policy (CSP)
A strong CSP constrains which scripts can execute on your pages. Because XSS can be used to steal CSRF tokens or make authenticated same-origin requests from within the page, preventing XSS with CSP is one of the most effective supplementary CSRF controls:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'
X-Frame-Options / frame-ancestors
Clickjacking attacks can trick victims into clicking invisible buttons on an embedded iframe of your application, effectively forcing CSRF-like actions without the victim’s awareness. Preventing your pages from being framed removes this vector:
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
Strict-Transport-Security (HSTS)
CSRF attacks require the victim to have a valid authenticated session. HSTS ensures that session cookies are never transmitted over plaintext HTTP, which prevents interception and cookie injection over local networks:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Establishing a Team-Wide CSRF Policy
In a growing codebase or a team with diverse security backgrounds, a documented CSRF policy prevents protection gaps from accumulating:
- Adopt framework defaults: Use frameworks that enable CSRF protection by default (Django, Rails, Spring Security, ASP.NET Core) and document that protection is always on. Developers should not need to opt in to security.
- Formal exemption process: Define requirements for exempting endpoints from CSRF checks. Any exemption should require documented justification, an alternative authentication mechanism (such as HMAC request-body signatures for webhooks), and a dated risk-acceptance sign-off.
- Code review checklist: Add a CSRF line to your PR review template. Every new route or controller action that performs a state change should be verified to include CSRF protection before it merges.
- Threat model updates: Revisit your CSRF threat model after major architecture changes—adding an API gateway, adopting a new frontend framework, or migrating to JWTs. Each change can silently alter which protections are active.
- Security champion programme: Designate a security-aware developer per team who is responsible for staying current on CSRF and related web security developments and communicating them to the team.
CSRF in Modern Single-Page Applications
Single-page applications (SPAs) that communicate with dedicated REST or GraphQL APIs face a slightly different threat model than traditional server-rendered applications. Understanding where the differences lie is essential to avoid misconfigured or missing protection.
Why Cookie-Based Auth in SPAs Still Needs CSRF Protection
A common misconception is that SPAs using JWTs stored in localStorage are immune to CSRF because localStorage values are not sent automatically with cross-origin requests. This is true—but only when the JWT lives exclusively in localStorage. Many security-conscious teams store JWT access tokens in HttpOnly cookies (to prevent XSS from stealing them), which reintroduces the CSRF risk, because those cookies are sent automatically by the browser with every request to the origin.
The rule of thumb: if your SPA authenticates via cookies—session cookies or JWT cookies—you need CSRF protection on every state-changing API endpoint.
The Cookie-to-Header Pattern in Frameworks
Angular, React, and Vue applications commonly implement the cookie-to-header pattern:
- The server sets a non-
HttpOnlyCSRF token cookie (e.g.,XSRF-TOKEN) so JavaScript can read it. - The SPA reads the cookie value using JavaScript.
- Every mutating API call includes the token as a custom header (e.g.,
X-XSRF-TOKEN). - The server validates that the header token matches the cookie token.
Angular’s HttpClient implements this automatically when you configure it:
// app.config.ts
import { provideHttpClient, withXsrfConfiguration } from '@angular/common/http'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withXsrfConfiguration({
cookieName: 'XSRF-TOKEN',
headerName: 'X-XSRF-TOKEN'
})
)
]
}
For React and Vue, an Axios interceptor achieves the same result:
// axios-setup.js
import axios from 'axios'
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
return match ? match[2] : ''
}
axios.interceptors.request.use((config) => {
if (!/^(GET|HEAD|OPTIONS)$/i.test(config.method)) {
config.headers['X-XSRF-TOKEN'] = getCookie('XSRF-TOKEN')
}
return config
})
Custom Request Headers for JSON APIs
If your API exclusively accepts Content-Type: application/json, a custom request header provides an additional layer of protection by triggering a CORS preflight. Cross-site pages cannot set arbitrary headers without a preflight exchange, and the server can reject unexpected origins at the CORS layer. However, this should be treated as a secondary defense, not a primary one. Browser quirks, content-type flexibility, and future API changes can silently remove this protection. Always combine it with explicit CSRF tokens or SameSite cookies.
JWTs in Authorization Headers
If your SPA stores the JWT exclusively in localStorage and sends it as a Bearer token in the Authorization header, your API is inherently resistant to classical CSRF—cross-site requests cannot programmatically set this header. In this architecture, ensure:
- The JWT is never stored in a cookie.
- Your CORS configuration allows credentials only from explicitly trusted origins, not
Access-Control-Allow-Origin: *. - You still defend against XSS, which can steal
localStoragevalues and impersonate users in same-origin JavaScript calls.
CSRF Attack Flow in a SPA Context
sequenceDiagram
actor Victim
participant SPA as SPA (example.com)
participant AttackerPage as Attacker Page (evil.com)
participant API as REST API (api.example.com)
Victim->>SPA: Logs in
API-->>SPA: Set-Cookie: session=xyz; SameSite=Lax; HttpOnly
API-->>SPA: Set-Cookie: XSRF-TOKEN=abc123 (readable by JS)
Note over Victim,SPA: SPA reads XSRF-TOKEN and attaches it to requests
Victim->>AttackerPage: Visits malicious page
AttackerPage->>API: POST /transfer (no X-XSRF-TOKEN header)
Note right of AttackerPage: Cannot read XSRF-TOKEN cookie from evil.com
API-->>AttackerPage: HTTP 403 Forbidden
Note over API: Attack blocked
CSRF in GraphQL APIs
GraphQL APIs introduce a unique challenge: all queries and mutations typically go to a single endpoint (/graphql) over POST, blurring the traditional distinction between read and write operations. Despite this, GraphQL endpoints are vulnerable to CSRF whenever:
- Session authentication is handled via cookies.
- The server accepts
application/x-www-form-urlencodedortext/plaincontent types in addition toapplication/json.
Mean applications where GraphQL accepts only application/json requests have a degree of natural CSRF resistance, because a cross-site form cannot set that content type. However, this should be explicitly enforced, not assumed. Best practices for GraphQL CSRF protection:
- Reject non-JSON content types at the middleware level—return 415 Unsupported Media Type for anything other than
application/json. - Add CSRF tokens to the GraphQL request via a custom header (
X-CSRF-Token) or as a field in the operation’s variables. - Enable
SameSite=Stricton session cookies to add a browser-level barrier. - Validate the
Originheader on every mutation to ensure only your known domains can trigger writes.
CSRF Over WebSockets
WebSocket handshakes are initiated via HTTP GET requests, which carry cookies. If an attacker can trick a victim’s browser into opening a WebSocket connection to a vulnerable server, the handshake completes with the victim’s session cookie attached—effectively giving the attacker an authenticated WebSocket channel.
Mitigating WebSocket CSRF:
- Validate the
Originheader on the upgrade request: The WebSocket server should reject upgrade requests where theOriginheader does not match an explicitly allowed list. - Issue a short-lived connection token: On the server, generate a one-time token, embed it in the page, and require it as the first message after the WebSocket connection opens. If the token is missing or invalid, close the connection.
// Server-side (Node.js + ws)
wss.on('connection', (socket, request) => {
const origin = request.headers['origin']
const allowedOrigins = ['https://app.example.com']
if (!allowedOrigins.includes(origin)) {
socket.close(4003, 'Forbidden origin')
return
}
// Optionally: require a one-time token as first message
socket.once('message', (msg) => {
const { csrfToken } = JSON.parse(msg)
if (!isValidCsrfToken(csrfToken, request)) {
socket.close(4003, 'Invalid CSRF token')
}
})
})
Third-Party Login Flows and OAuth
OAuth 2.0 authorization flows introduce a specialized CSRF risk. During an OAuth login, the authorization server redirects the user back to your application with an authorization code. An attacker can intercept this redirect and trick a victim’s browser into completing the code exchange, logging the victim in as the attacker (the login CSRF scenario in an OAuth context).
The OAuth specification addresses this with the state parameter:
- Before redirecting the user to the authorization server, generate a random
statevalue and store it in the session. - Include
statein the authorization request URL. - When the authorization server redirects back, verify that the returned
statematches the value stored in the session. - If the values do not match, reject the callback request entirely.
Many OAuth libraries handle this automatically, but it is worth auditing your OAuth integration to confirm state validation is active and cannot be bypassed by omitting the parameter.
Conclusion
Cross-Site Request Forgery is a serious threat to web application security, but it is entirely preventable with the right strategies. By implementing CSRF tokens, leveraging the SameSite attribute, and adhering to secure coding practices, developers can protect their applications and users from unauthorized actions.
The key takeaways from this guide are:
- Understand the mechanism: CSRF exploits the browser’s automatic cookie attachment. Any endpoint that changes state and uses cookie authentication is a potential target.
- Layer your defenses: Synchronizer tokens or signed double-submit cookies at the server layer,
SameSite=StrictorLaxat the cookie layer, andOriginheader verification as a secondary check provide robust, redundant protection. - Use your framework: Modern frameworks including Django, Rails, Spring Security, and ASP.NET Core ship with CSRF protection enabled. Prefer these over rolling your own implementation, and never disable them globally.
- Extend to new paradigms: SPAs, REST APIs, GraphQL endpoints, and WebSocket handshakes all require deliberate CSRF consideration. The same principles apply, but the implementation details differ—review each surface when adopting a new architecture.
- Test and automate: Include CSRF checks in your code review process, penetration testing scope, and CI/CD pipelines. Catching a regression during development costs a fraction of addressing it post-deployment.
- Stay current: Browser vendors continue to evolve
SameSitebehaviour, introduce new security headers, and deprecate legacy workarounds. Subscribe to security advisories from OWASP and your framework maintainers to stay ahead of changes that affect your threat model.
Proactive testing and a commitment to security ensure your application remains resilient against CSRF and other evolving threats. Start strengthening your defenses today to build trust and reliability into your applications.