CSRF Mitigation Strategies and Best Practices¶
Introduction to CSRF Mitigation¶
Cross-Site Request Forgery (CSRF) attacks can be prevented through multiple layers of defense. The most effective approach combines several techniques to create defense-in-depth. This guide covers comprehensive mitigation strategies, from basic implementations to advanced protection mechanisms.
Primary Defense Mechanisms¶
1. CSRF Tokens (Synchronizer Token Pattern)¶
How It Works¶
CSRF tokens are unique, cryptographically secure values that are: - Generated server-side for each user session - Included in legitimate forms and requests - Validated on the server before processing state-changing operations
Implementation Examples¶
PHP Implementation:
<?php
session_start();
// Generate CSRF token
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrf_token = $_SESSION['csrf_token'];
?>
<!-- In HTML form -->
<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token" value="<?php echo $csrf_token; ?>">
<input type="text" name="amount">
<input type="text" name="to_account">
<button type="submit">Transfer</button>
</form>
<!-- Server-side validation -->
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('CSRF token validation failed');
}
// Process the request
transfer_money($_POST['amount'], $_POST['to_account']);
}
?>
Node.js/Express with csurf:
const express = require('express');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const app = express();
app.use(cookieParser());
app.use(express.urlencoded({ extended: true }));
// CSRF protection middleware
const csrfProtection = csrf({ cookie: true });
// Apply to all POST routes
app.use('/api', csrfProtection);
// In route handler
app.post('/transfer', (req, res) => {
// CSRF token is automatically validated
const { amount, toAccount } = req.body;
transferMoney(amount, toAccount);
res.json({ success: true });
});
// Get CSRF token for forms
app.get('/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
Python/Django (Built-in):
# settings.py
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
'django.middleware.security.SecurityMiddleware',
# ... other middleware
]
# In template
<form method="post">
{% csrf_token %}
<input type="text" name="amount">
<input type="text" name="to_account">
<button type="submit">Transfer</button>
</form>
# In view
from django.views.decorators.csrf import csrf_protect
@csrf_protect
def transfer_view(request):
if request.method == 'POST':
# CSRF token automatically validated
amount = request.POST.get('amount')
to_account = request.POST.get('to_account')
transfer_money(amount, to_account)
return render(request, 'transfer.html')
Ruby on Rails (Built-in):
# ApplicationController
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
# In view
<%= form_for @transfer, url: transfer_path do |f| %>
<%= f.hidden_field :authenticity_token %>
<%= f.text_field :amount %>
<%= f.text_field :to_account %>
<%= f.submit "Transfer" %>
<% end %>
# In controller
class TransfersController < ApplicationController
def create
# CSRF token automatically validated
@transfer = Transfer.new(transfer_params)
if @transfer.save
redirect_to @transfer, notice: 'Transfer completed.'
else
render :new
end
end
private
def transfer_params
params.require(:transfer).permit(:amount, :to_account)
end
end
Token Security Best Practices¶
- Cryptographically Secure Generation:
- Use
random_bytes()or equivalent - Minimum 32 bytes of entropy
-
Unique per session/form
-
Token Scope:
- Per-session tokens for simplicity
- Per-form tokens for maximum security
-
Per-request tokens for high-security operations
-
Token Storage:
- Server-side session storage
- Avoid client-side storage (localStorage, cookies)
-
Regenerate after successful authentication
-
Token Validation:
- Strict comparison (
===in PHP,==in Python) - Immediate failure on mismatch
- Log validation failures for monitoring
2. SameSite Cookie Attribute¶
How It Works¶
The SameSite attribute controls when cookies are sent with cross-origin requests: - Strict: Cookies only sent for same-origin requests - Lax: Cookies sent for top-level navigation (GET requests) - None: Cookies sent for all cross-origin requests (requires Secure)
Implementation¶
Setting SameSite in PHP:
// Set session cookie with SameSite
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => 'example.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]);
Setting SameSite in Express:
app.use(session({
secret: 'your-secret',
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
sameSite: 'lax'
}
}));
Setting SameSite in Django:
# settings.py
SESSION_COOKIE_SAMESITE = 'Lax'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
Setting SameSite in Rails:
# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store, key: "_app_session", same_site: :lax, secure: true
Browser Support and Fallbacks¶
// JavaScript fallback for older browsers
function setCookie(name, value, options = {}) {
let cookieString = `${name}=${value}`;
if (options.sameSite) {
// Modern browsers
cookieString += `; SameSite=${options.sameSite}`;
}
// Fallback for older browsers - use additional checks
if (options.secure) cookieString += '; Secure';
if (options.httpOnly) cookieString += '; HttpOnly';
if (options.path) cookieString += `; Path=${options.path}`;
if (options.maxAge) cookieString += `; Max-Age=${options.maxAge}`;
document.cookie = cookieString;
}
3. Referer Header Validation¶
How It Works¶
Validate the Referer header to ensure requests come from trusted origins: - Check if the referer matches the expected domain - Reject requests with missing or suspicious referers - Combine with HTTPS to prevent header spoofing
Implementation¶
PHP Referer Validation:
function validateReferer($allowed_domains = []) {
if (!isset($_SERVER['HTTP_REFERER'])) {
return false;
}
$referer = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
foreach ($allowed_domains as $domain) {
if (stripos($referer, $domain) !== false) {
return true;
}
}
return false;
}
// Usage
if (!validateReferer(['example.com', 'www.example.com'])) {
die('Invalid referer');
}
Express Referer Middleware:
const express = require('express');
const app = express();
function validateReferer(req, res, next) {
const referer = req.get('Referer');
const allowedDomains = ['https://example.com', 'https://www.example.com'];
if (!referer) {
return res.status(403).json({ error: 'Missing referer header' });
}
const refererOrigin = new URL(referer).origin;
if (!allowedDomains.includes(refererOrigin)) {
return res.status(403).json({ error: 'Invalid referer' });
}
next();
}
// Apply to sensitive routes
app.post('/transfer', validateReferer, (req, res) => {
// Process transfer
});
Django Referer Validation:
from django.middleware.csrf import CsrfViewMiddleware
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
class RefererValidationMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.allowed_domains = ['example.com', 'www.example.com']
def __call__(self, request):
if request.method in ['POST', 'PUT', 'DELETE', 'PATCH']:
referer = request.META.get('HTTP_REFERER')
if referer:
try:
referer_domain = urlparse(referer).netloc
if not any(domain in referer_domain for domain in self.allowed_domains):
return HttpResponseForbidden('Invalid referer')
except:
return HttpResponseForbidden('Invalid referer')
else:
return HttpResponseForbidden('Missing referer')
return self.get_response(request)
Limitations and Considerations¶
- HTTPS Required: Referer headers can be stripped over HTTP
- Browser Behavior: Some browsers don't send referer for HTTPS → HTTP
- Privacy Extensions: Some users disable referer sending
- Mobile Apps: May not send referer headers
- CDNs/Proxies: May modify or strip referer headers
4. Custom Header Requirements¶
How It Works¶
Require specific headers that can only be set by legitimate requests: - Custom headers like X-Requested-With: XMLHttpRequest - Headers that browsers don't automatically include - Headers that must be explicitly set by legitimate code
Implementation¶
Express Custom Header Check:
function requireCustomHeader(req, res, next) {
const customHeader = req.get('X-Custom-Header');
if (!customHeader || customHeader !== 'expected-value') {
return res.status(403).json({ error: 'Missing or invalid custom header' });
}
next();
}
// Apply to API endpoints
app.post('/api/transfer', requireCustomHeader, (req, res) => {
// Process transfer
});
Angular HTTP Client:
import { HttpClient, HttpHeaders } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class ApiService {
private headers = new HttpHeaders({
'X-Custom-Header': 'expected-value',
'Content-Type': 'application/json'
});
constructor(private http: HttpClient) {}
transferMoney(amount: number, toAccount: string) {
return this.http.post('/api/transfer', {
amount,
toAccount
}, { headers: this.headers });
}
}
React Fetch with Custom Headers:
const apiCall = async (endpoint, data) => {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'expected-value',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(data)
});
return response.json();
};
Advanced Mitigation Techniques¶
1. Double Submit Cookie Pattern¶
How It Works¶
- Set a random value in both a cookie and a request parameter
- Server validates that both values match
- Prevents CSRF while avoiding server-side state
Implementation¶
JavaScript Client:
// Generate random token
function generateToken() {
return Math.random().toString(36).substring(2);
}
const csrfToken = generateToken();
// Set cookie
document.cookie = `csrf_token=${csrfToken}; Secure; SameSite=Lax`;
// Include in request
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 1000,
toAccount: '12345',
csrfToken: csrfToken
})
});
Server Validation:
app.post('/api/transfer', (req, res) => {
const cookieToken = req.cookies.csrf_token;
const requestToken = req.body.csrfToken;
if (!cookieToken || !requestToken || cookieToken !== requestToken) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
// Process request
});
2. Origin Header Validation¶
How It Works¶
Validate the Origin header (more reliable than Referer): - Check if the origin matches allowed domains - Reject requests from unauthorized origins - Combine with other validation methods
Implementation¶
Express Origin Validation:
function validateOrigin(req, res, next) {
const origin = req.get('Origin');
const allowedOrigins = [
'https://example.com',
'https://www.example.com',
'https://app.example.com'
];
if (origin && !allowedOrigins.includes(origin)) {
return res.status(403).json({ error: 'Invalid origin' });
}
next();
}
3. Content-Type Validation¶
How It Works¶
Restrict requests to specific content types: - Only allow application/json for API endpoints - Reject form-encoded requests from different origins - Prevent simple form submissions
Implementation¶
Express Content-Type Check:
function requireJsonContentType(req, res, next) {
const contentType = req.get('Content-Type');
if (!contentType || !contentType.includes('application/json')) {
return res.status(415).json({ error: 'Content-Type must be application/json' });
}
next();
}
// Apply to API routes
app.use('/api', requireJsonContentType);
4. Request Method Validation¶
How It Works¶
Ensure state-changing operations use appropriate HTTP methods: - Use POST/PUT/PATCH/DELETE for state changes - Avoid GET for any state-changing operations - Validate method consistency
Implementation¶
Method Validation Middleware:
function validateMethod(req, res, next) {
const allowedMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];
if (!allowedMethods.includes(req.method)) {
return res.status(405).json({ error: 'Method not allowed' });
}
next();
}
Framework-Specific Mitigations¶
1. Spring Security CSRF Protection¶
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
}
2. ASP.NET Core CSRF Protection¶
// Startup.cs
public void ConfigureServices(IServiceCollection services) {
services.AddControllersWithViews(options => {
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
services.AddAntiforgery(options => {
options.HeaderName = "X-CSRF-TOKEN";
});
}
// In view
<form asp-action="Transfer" asp-controller="Account" method="post">
@Html.AntiForgeryToken()
<input asp-for="Amount" />
<input asp-for="ToAccount" />
<button type="submit">Transfer</button>
</form>
3. Laravel CSRF Protection¶
// In routes/web.php
Route::post('/transfer', function (Request $request) {
// CSRF token automatically validated
$amount = $request->input('amount');
$toAccount = $request->input('to_account');
// Process transfer
})->middleware('web'); // Includes CSRF protection
// In blade template
<form method="POST" action="/transfer">
@csrf
<input type="text" name="amount">
<input type="text" name="to_account">
<button type="submit">Transfer</button>
</form>
API-Specific Mitigations¶
1. REST API CSRF Protection¶
// API endpoint with multiple protections
app.post('/api/users/:id', [
validateOrigin,
validateCustomHeader,
requireJsonContentType
], (req, res) => {
const userId = req.params.id;
const updates = req.body;
// Additional authentication check
if (req.user.id !== userId) {
return res.status(403).json({ error: 'Unauthorized' });
}
updateUser(userId, updates);
res.json({ success: true });
});
2. GraphQL CSRF Protection¶
const { graphqlHTTP } = require('express-graphql');
const { validateCsrfToken } = require('./csrf-middleware');
app.use('/graphql', graphqlHTTP({
schema: MyGraphQLSchema,
rootValue: root,
validationRules: [validateCsrfToken]
}));
3. WebSocket CSRF Protection¶
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
// Validate CSRF token from query parameter or header
const csrfToken = req.url.split('csrf=')[1];
if (!validateCsrfToken(csrfToken)) {
ws.close(1008, 'CSRF validation failed');
return;
}
// Handle WebSocket messages
ws.on('message', (message) => {
// Process message
});
});
Mobile Application Mitigations¶
1. Android CSRF Protection¶
// OkHttp client with CSRF token
public class ApiClient {
private static final String CSRF_TOKEN = "X-CSRF-Token";
public static void makeRequest(String url, JSONObject data) throws IOException {
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new CsrfInterceptor())
.build();
RequestBody body = RequestBody.create(
data.toString(),
MediaType.parse("application/json")
);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
client.newCall(request).execute();
}
static class CsrfInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request.Builder builder = original.newBuilder()
.header(CSRF_TOKEN, getCsrfToken());
return chain.proceed(builder.build());
}
}
}
2. iOS CSRF Protection¶
import Alamofire
class ApiManager {
static let csrfToken = "X-CSRF-Token"
func makeRequest(url: String, parameters: [String: Any]) {
let headers: HTTPHeaders = [
csrfToken: getCsrfToken(),
"Content-Type": "application/json"
]
AF.request(url,
method: .post,
parameters: parameters,
encoding: JSONEncoding.default,
headers: headers)
.responseJSON { response in
// Handle response
}
}
}
Monitoring and Detection¶
1. CSRF Attack Detection¶
// Middleware to detect suspicious patterns
function detectCsrfAttempts(req, res, next) {
const suspiciousPatterns = [
req.method === 'POST' && !req.get('Referer'),
req.method === 'POST' && req.get('Origin') !== 'https://example.com',
req.body && typeof req.body === 'object' && !req.body.csrfToken
];
if (suspiciousPatterns.some(pattern => pattern)) {
// Log suspicious activity
console.log('Potential CSRF attempt:', {
ip: req.ip,
userAgent: req.get('User-Agent'),
url: req.url,
method: req.method
});
}
next();
}
2. Rate Limiting¶
const rateLimit = require('express-rate-limit');
const csrfLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Limit each IP to 5 requests per windowMs
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
// Apply to sensitive endpoints
app.use('/api/transfer', csrfLimiter);
3. Logging and Alerting¶
function logCsrfEvent(req, eventType, details) {
const logEntry = {
timestamp: new Date().toISOString(),
eventType,
ip: req.ip,
userAgent: req.get('User-Agent'),
url: req.url,
method: req.method,
referer: req.get('Referer'),
origin: req.get('Origin'),
details
};
// Send to logging service
logService.log('csrf_attempt', logEntry);
// Send alert if critical
if (eventType === 'csrf_success') {
alertService.sendAlert('CSRF Attack Successful', logEntry);
}
}
Testing CSRF Mitigations¶
1. Automated Testing¶
const puppeteer = require('puppeteer');
async function testCsrfProtection() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Navigate to legitimate site
await page.goto('https://example.com/login');
await page.type('#username', 'testuser');
await page.type('#password', 'testpass');
await page.click('#login');
// Wait for login
await page.waitForNavigation();
// Try CSRF attack
const csrfPage = await browser.newPage();
await csrfPage.setContent(`
<form action="https://example.com/transfer" method="POST">
<input name="amount" value="1000">
<input name="to" value="attacker">
</form>
<script>document.forms[0].submit();</script>
`);
// Check if attack succeeds
const response = await csrfPage.waitForResponse('https://example.com/transfer');
const status = response.status();
if (status === 403) {
console.log('CSRF protection working');
} else {
console.log('CSRF protection failed');
}
await browser.close();
}
2. Manual Testing Checklist¶
- Remove CSRF tokens from requests
- Test with different origins
- Try GET requests for POST operations
- Test with missing referer headers
- Attempt JSON requests without proper headers
- Test SameSite cookie bypasses
- Verify token regeneration after login
Common Pitfalls and Mistakes¶
1. Incomplete Protection¶
// Bad: Only protecting some routes
app.post('/transfer', csrfProtection, handler);
app.post('/settings', handler); // Missing protection
app.put('/profile', handler); // Missing protection
2. Weak Token Generation¶
// Bad: Predictable tokens
const token = Date.now().toString(); // Easily guessable
// Bad: Short tokens
const token = Math.random().toString(36).substring(2, 8); // Too short
3. Improper Token Validation¶
// Bad: Loose comparison
if (req.body.csrfToken != session.csrfToken) { // Type coercion
// Bad: No token rotation
// Token never changes after generation
4. CORS Misconfiguration¶
// Bad: Permissive CORS
app.use(cors({
origin: true, // Allows any origin
credentials: true // Allows cookies
}));
5. Missing HTTPS¶
// Bad: No HTTPS enforcement
app.listen(80); // HTTP only
// Bad: No secure cookie flags
res.cookie('session', sessionId); // Missing secure flag
Performance Considerations¶
1. Token Generation Overhead¶
// Optimized token generation
const crypto = require('crypto');
function generateSecureToken() {
// Use crypto.randomBytes for better performance than random_bytes
return crypto.randomBytes(32).toString('hex');
}
// Cache tokens for session
const tokenCache = new Map();
function getCachedToken(sessionId) {
if (!tokenCache.has(sessionId)) {
tokenCache.set(sessionId, generateSecureToken());
}
return tokenCache.get(sessionId);
}
2. Database Session Storage¶
// Efficient session storage
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
const sessionStore = new MySQLStore({
host: 'localhost',
user: 'session_user',
password: 'session_pass',
database: 'sessions'
});
app.use(session({
store: sessionStore,
secret: 'your-secret',
resave: false,
saveUninitialized: false
}));
Compliance and Standards¶
1. OWASP Recommendations¶
- Use CSRF tokens for all state-changing operations
- Implement SameSite=Lax for session cookies
- Validate Origin/Referer headers
- Log and monitor CSRF attempts
2. Industry Standards¶
NIST SP 800-63B: - Multi-factor authentication reduces CSRF impact - Session management best practices - Token-based authentication guidance
ISO 27001: - Access control requirements - Information security incident management - Cryptographic controls
Conclusion¶
Effective CSRF mitigation requires a multi-layered approach combining several techniques:
- CSRF Tokens as the primary defense mechanism
- SameSite Cookies for additional browser-level protection
- Referer/Origin Validation for request origin verification
- Custom Headers for API endpoint protection
- Proper HTTPS Configuration to prevent header manipulation
Modern web frameworks provide built-in CSRF protection, but custom implementations require careful attention to security best practices. Regular testing, monitoring, and staying updated with emerging threats are essential for maintaining effective CSRF protection.
Remember: Defense-in-depth is key. No single mitigation technique is foolproof, but combining multiple layers creates robust protection against CSRF attacks.