What is CSRF?
Cross-Site Request Forgery (CSRF) is a web security vulnerability that tricks an authenticated user's browser into sending unintended requests to a web application. The attacker exploits the trust that a website has in the user's browser, not the trust the user has in the website.
Unlike Cross-Site Scripting (XSS) which exploits the trust a user has for a particular site, CSRF exploits the trust a site has in a user's browser. When you are logged into a banking website, your browser automatically includes your session cookie with every request to that domain. An attacker can craft a malicious page that triggers requests to your bank, and your browser will dutifully attach your credentials.
You may see CSRF referred to as "session riding," "XSRF," or "one-click attack." The OWASP Top 10 has consistently listed it as a critical web application vulnerability.
How CSRF Attacks Work
A CSRF attack follows a predictable pattern. Understanding this chain is critical to building effective defenses.
Here is what a simple CSRF attack looks like in practice. An attacker could embed this invisible form on any website:
<!-- Hidden on attacker's website -->
<form action="https://bank.example.com/transfer" method="POST" id="csrf-form">
<input type="hidden" name="to_account" value="attacker-account-789" />
<input type="hidden" name="amount" value="5000" />
</form>
<script>document.getElementById('csrf-form').submit();</script>
When the victim loads this page, the form auto-submits. Their browser sends the POST request to the bank with their active session cookie, and the transfer executes without the victim ever clicking a button.
If a state-changing action uses GET (e.g., /delete?id=42), the attacker
only needs an image tag: <img src="https://target.com/delete?id=42">.
This is why state-changing operations must never use GET requests.
Real-World CSRF Examples
CSRF vulnerabilities have been found in major applications throughout web security history, demonstrating that even well-resourced teams can overlook this class of attack.
- Netflix (2006) - Attackers could change account email addresses and passwords through CSRF, effectively taking over accounts. The attack only required the victim to visit a malicious page while logged into Netflix.
- ING Direct (2008) - A CSRF vulnerability allowed attackers to open additional accounts, transfer funds between the victim's accounts, and then withdraw from the newly created account.
- YouTube (2008) - Researchers demonstrated CSRF attacks that could subscribe victims to channels, add videos to favorites, and send messages on behalf of the victim without any interaction.
- Router administration panels - Many home routers have been vulnerable to CSRF attacks that change DNS settings, effectively redirecting all of the victim's traffic through an attacker-controlled server.
Traditional CSRF attacks target browser-based sessions with cookies. APIs that use token-based authentication (like Bearer tokens in the Authorization header) are generally not vulnerable to CSRF because the browser does not automatically attach these tokens. However, APIs that rely on cookies for authentication remain at risk.
CSRF Tokens
The most widely used defense against CSRF is the synchronizer token pattern. The server generates a unique, unpredictable token for each session (or each form), embeds it in the HTML, and validates it when the form is submitted. An attacker cannot read this token from a cross-origin page, so they cannot forge a valid request.
Server-Side Token Generation (Python/Flask example)
import secrets
@app.before_request
def generate_csrf_token():
if 'csrf_token' not in session:
session['csrf_token'] = secrets.token_hex(32)
@app.route('/transfer', methods=['POST'])
def transfer():
token = request.form.get('csrf_token')
if not token or token != session.get('csrf_token'):
abort(403, 'CSRF token validation failed')
# Process the legitimate request
process_transfer(request.form)
HTML Form with CSRF Token
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="{{ session.csrf_token }}" />
<label for="to_account">Recipient Account:</label>
<input type="text" name="to_account" id="to_account" />
<label for="amount">Amount:</label>
<input type="number" name="amount" id="amount" />
<button type="submit">Transfer</button>
</form>
CSRF Tokens for AJAX Requests
For JavaScript-driven applications, the token is typically sent in a custom HTTP header:
// Read token from meta tag or cookie
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ to_account: '12345', amount: 100 })
});
The server validates the X-CSRF-Token header against the session token.
Cross-origin requests cannot set custom headers without CORS permission, which provides
an additional layer of protection.
SameSite Cookies
The SameSite cookie attribute is a browser-level defense that controls whether
cookies are sent with cross-site requests. It is one of the most effective modern mitigations
against CSRF.
Secure flag. Only use this when cross-site cookie access is explicitly needed (e.g., embedded iframes, third-party integrations).
# Setting SameSite cookies (HTTP response header)
Set-Cookie: session_id=abc123; SameSite=Lax; Secure; HttpOnly; Path=/
# In Express.js
app.use(session({
cookie: {
sameSite: 'lax',
secure: true,
httpOnly: true
}
}));
# In PHP
session_set_cookie_params([
'samesite' => 'Lax',
'secure' => true,
'httponly' => true
]);
While SameSite=Lax blocks most CSRF attacks, it still allows GET-based CSRF through top-level navigation. If your application has any state-changing GET endpoints, SameSite=Lax will not protect them. Always combine SameSite cookies with CSRF tokens for defense in depth.
Defense in Depth
No single defense is bulletproof. A robust CSRF protection strategy layers multiple controls so that if one mechanism fails, others remain effective.
- CSRF tokens - Include unique, per-session (or per-request) tokens in every state-changing form and validate them server-side
- SameSite cookies - Set
SameSite=Lax(orStrict) on all session cookies to prevent cross-site cookie transmission - Custom request headers - Require a custom header (e.g.,
X-Requested-With) on API calls; browsers block cross-origin custom headers without CORS - Origin/Referer validation - Check the
OriginorRefererheader to verify requests come from your own domain - Re-authentication for sensitive actions - Require password or MFA confirmation for high-impact operations like password changes or large transfers
- Use POST for state changes - Never use GET requests for actions that modify data; this prevents trivial image-tag-based CSRF
Origin Header Validation Example
def validate_origin(request):
allowed_origins = ['https://yourapp.example.com']
origin = request.headers.get('Origin')
referer = request.headers.get('Referer')
# Origin header is present on POST/PUT/DELETE
if origin:
return origin in allowed_origins
# Fall back to Referer for GET requests
if referer:
from urllib.parse import urlparse
parsed = urlparse(referer)
return f"{parsed.scheme}://{parsed.netloc}" in allowed_origins
# If neither header is present, reject the request
return False
Testing for CSRF
Testing your application for CSRF vulnerabilities is essential before deployment. Here are the key steps and tools to verify your defenses.
Manual Testing Checklist
- Identify all state-changing endpoints (forms, API calls that modify data)
- For each endpoint, attempt to submit a request from a different origin without a CSRF token
- Verify that requests without valid CSRF tokens are rejected with a 403 status
- Test whether tokens are tied to the session (try using a token from a different session)
- Confirm that GET requests cannot trigger state changes
- Check that SameSite cookie attributes are set correctly using browser developer tools
Using Browser Developer Tools
# In the browser console, check cookie attributes:
# Open DevTools > Application > Cookies
# Verify each session cookie shows:
# SameSite: Lax (or Strict)
# Secure: true (checkmark)
# HttpOnly: true (checkmark)
# Test a forged request from the console:
fetch('https://yourapp.com/api/transfer', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 100 })
});
# This should be rejected (403) if CSRF protection is working
Automated Tools
- OWASP ZAP - Free, open-source scanner that automatically identifies CSRF vulnerabilities in forms and AJAX endpoints
- Burp Suite - Professional-grade tool with a CSRF PoC generator that creates attack pages to verify exploitability
- CSRFTester - Dedicated OWASP tool for generating and testing CSRF proof-of-concept attacks
Most modern web frameworks include built-in CSRF protection. Django has
{% csrf_token %}, Rails has protect_from_forgery, Laravel has
@csrf, and Express has csurf middleware. Always enable your
framework's CSRF protection rather than building your own from scratch.
Summary
In this tutorial, you learned:
- What CSRF is and why it exploits the browser's automatic cookie behavior
- The step-by-step mechanics of a CSRF attack, from authentication to forged request
- Real-world CSRF vulnerabilities found in Netflix, YouTube, and banking applications
- How to implement CSRF tokens in both traditional forms and AJAX requests
- The three SameSite cookie modes (Strict, Lax, None) and when to use each
- Defense-in-depth strategies combining tokens, cookies, headers, and re-authentication
- Manual and automated approaches to testing for CSRF vulnerabilities
By combining CSRF tokens, SameSite cookies, and proper request validation, your web applications will be resilient against cross-site request forgery. Always test your defenses and use your framework's built-in protections when available.