Cross-Site Request Forgery (CSRF)
Cross-Site Request Forgery (CSRF) is an attack that forces authenticated users to submit requests they did not intend to make. Ranked in OWASP Top 10, CSRF exploits the trust that a web application has in the user's browser by leveraging automatically included credentials (cookies, HTTP authentication). Attackers craft malicious requests that appear legitimate because they carry the victim's authentication credentials. Successful CSRF attacks can transfer funds, change account settings, modify data, or perform any state-changing operation the victim is authorized to perform. Unlike XSS which targets the user, CSRF targets the web application itself.
What Is CSRF?
CSRF exploits the trust that a web application has in the user's browser. When a user is authenticated to a site, their browser automatically includes authentication cookies with every request. Attackers abuse this by creating malicious requests that the browser sends on the user's behalf.
How CSRF Works:
- 1.User logs into legitimate site (bank.com) and receives session cookie
- 2.User visits malicious site (evil.com) while still logged in
- 3.Malicious site triggers hidden request to bank.com
- 4.Browser automatically includes authentication cookie
- 5.Bank executes request as if user intended it (transfer money, etc.)
CSRF Attack Examples
Hidden Form Attack
// Backend endpoint - NO CSRF protection
app.post('/transfer-money', (req, res) => {
const { toAccount, amount } = req.body;
const userId = req.session.userId; // From cookie
// PROBLEM: No verification this is intentional user action
transferMoney(userId, toAccount, amount);
res.json({ success: true });
});<!-- Hidden form on attacker's site -->
<html>
<body>
<h1>Check out this funny cat video!</h1>
<!-- Invisible form -->
<form id="attack" action="https://bank.com/transfer-money" method="POST">
<input type="hidden" name="toAccount" value="attacker123" />
<input type="hidden" name="amount" value="1000" />
</form>
<script>
// Auto-submit when page loads
document.getElementById('attack').submit();
</script>
<!-- User sees cat video, never knows money was transferred -->
</body>
</html>Image Tag Attack (GET Request)
// Backend endpoint using GET for state change (BAD!)
app.get('/delete-account', (req, res) => {
const userId = req.session.userId;
// PROBLEM: GET should be safe, but this modifies state
deleteUserAccount(userId);
res.json({ success: true });
});<!-- Attacker embeds this in email or forum post -->
<img src="https://yoursite.com/delete-account" width="1" height="1" />
<!-- Browser automatically makes GET request with cookies
User's account gets deleted without them knowing -->CSRF Prevention Methods
1. CSRF Tokens (Synchronizer Token)
Most CommonGenerate unique token for each session/request and validate it on the server.
// Backend - Express with csurf middleware
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
// Send CSRF token to client
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/transfer-money', csrfProtection, (req, res) => {
// Token automatically validated by middleware
const { toAccount, amount } = req.body;
transferMoney(req.session.userId, toAccount, amount);
res.json({ success: true });
});
// Frontend - Include token in form
<form action="/transfer-money" method="POST">
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
<input name="toAccount" />
<input name="amount" />
<button type="submit">Transfer</button>
</form>
// Or in AJAX header
fetch('/transfer-money', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
});2. SameSite Cookie Attribute
Modern BrowsersPrevent browser from sending cookies in cross-site requests.
// Backend - Set SameSite cookie attribute
res.cookie('sessionId', token, {
httpOnly: true,
secure: true,
sameSite: 'strict' // or 'lax'
});
// SameSite=Strict: Never send cookie in cross-site requests
// SameSite=Lax: Send cookie on top-level navigation (GET only)
// SameSite=None: Send always (requires Secure flag)Note: SameSite=Strict may break legitimate cross-site flows (OAuth, payment gateways). Use SameSite=Lax as a good default.
3. Double Submit Cookie Pattern
StatelessSend CSRF token in both a cookie and request parameter. Server validates they match. Useful for stateless APIs.
// Backend - Generate and send token
const csrfToken = crypto.randomBytes(32).toString('hex');
// Set as cookie
res.cookie('csrf-token', csrfToken, {
httpOnly: false, // Client needs to read it
secure: true,
sameSite: 'strict'
});
// Also send in response body
res.json({ csrfToken });
// Client includes token in request header
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
});
// Server validates cookie matches header
app.post('/api/transfer', (req, res) => {
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
// Process request
});4. Custom Request Headers
AJAX OnlyRequire custom header that can only be set by JavaScript on same origin. Simple forms cannot send custom headers.
// Client - Send custom header
fetch('/api/data', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(data)
});
// Server - Validate header presence
app.post('/api/data', (req, res) => {
if (req.headers['x-requested-with'] !== 'XMLHttpRequest') {
return res.status(403).json({ error: 'Invalid request' });
}
// Process request
});
// Note: Only works for AJAX requests, not regular formsFramework CSRF Protection
Express.js with csurf Middleware
const express = require('express');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const app = express();
const csrfProtection = csrf({ cookie: true });
app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));
// Send CSRF token to client
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
// Validate CSRF token automatically
app.post('/process', csrfProtection, (req, res) => {
// Token validation happens in middleware
res.send('Data processed successfully');
});
// For AJAX requests
app.get('/api/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});Next.js API Routes
// pages/api/csrf-token.ts
import { NextApiRequest, NextApiResponse } from 'next';
import csrf from 'csrf';
const tokens = new csrf();
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const secret = process.env.CSRF_SECRET || 'default-secret';
const token = tokens.create(secret);
res.setHeader(
'Set-Cookie',
`csrf-token=${token}; HttpOnly=false; Secure; SameSite=Strict; Path=/`
);
res.json({ csrfToken: token });
}
// pages/api/protected-action.ts
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const secret = process.env.CSRF_SECRET || 'default-secret';
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'] as string;
if (!tokens.verify(secret, headerToken || cookieToken)) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
// Process protected action
res.json({ success: true });
}Django (Python)
# settings.py - CSRF middleware enabled by default
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
# other middleware
]
# views.py
from django.views.decorators.csrf import csrf_protect
@csrf_protect
def transfer_money(request):
if request.method == 'POST':
# CSRF token validated automatically
amount = request.POST.get('amount')
# Process transfer
return JsonResponse({'success': True})
# Template - Include CSRF token in form
{% csrf_token %}
<form method="POST" action="/transfer">
{% csrf_token %}
<input name="amount" />
<button type="submit">Transfer</button>
</form>
# For AJAX requests
<script>
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
},
body: JSON.stringify({ amount: 100 })
});
</script>Ruby on Rails
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# CSRF protection enabled by default
protect_from_forgery with: :exception
# For API endpoints (use null_session or reset_session)
# protect_from_forgery with: :null_session
end
# In views - Include CSRF token automatically
<%= form_with url: "/transfer" do |form| %>
<%= form.number_field :amount %>
<%= form.submit "Transfer" %>
<% end %>
# For AJAX - Rails includes token in meta tags
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="<%= form_authenticity_token %>" />
<script>
const token = document.querySelector('[name="csrf-token"]').content;
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': token,
'Content-Type': 'application/json'
},
body: JSON.stringify({ amount: 100 })
});
</script>Origin and Referer Header Validation
Validate Request Origin
Check Origin or Referer headers to ensure requests come from your domain. Use as defense-in-depth alongside tokens.
// Express middleware to validate origin
function validateOrigin(req, res, next) {
const allowedOrigins = [
'https://yourdomain.com',
'https://www.yourdomain.com'
];
// Check Origin header (preferred)
const origin = req.headers.origin;
if (origin && !allowedOrigins.includes(origin)) {
return res.status(403).json({ error: 'Invalid origin' });
}
// Fallback to Referer header
const referer = req.headers.referer || req.headers.referrer;
if (referer) {
const refererUrl = new URL(referer);
const refererOrigin = `${refererUrl.protocol}//${refererUrl.host}`;
if (!allowedOrigins.includes(refererOrigin)) {
return res.status(403).json({ error: 'Invalid referer' });
}
}
next();
}
app.post('/api/protected', validateOrigin, csrfProtection, (req, res) => {
// Process request
});
// Note: Not foolproof (headers can be stripped), use with other protectionsCSRF Prevention Best Practices
Use POST for State Changes
Never use GET for operations that modify data
Implement CSRF Tokens
Use framework-provided CSRF protection (Django, Rails, Express)
Set SameSite Cookies
Use SameSite=Lax or Strict on session cookies
Validate Origin Header
Check Origin/Referer matches your domain
Use HTTPOnly Cookies
Prevent JavaScript access to cookies
Require Re-auth
Ask for password on sensitive operations
How CodeRaptor Detects CSRF Vulnerabilities
CodeRaptor automatically scans for missing CSRF protection on state-changing endpoints.