Understanding and Preventing CSRF Attacks
Introduction
Cross-Site Request Forgery (CSRF) is an attack that tricks a victim into executing unwanted actions on a web application where they’re authenticated. Despite being well-known, CSRF vulnerabilities continue to appear in modern applications. This article explains how CSRF attacks work and demonstrates effective prevention techniques.
How CSRF Attacks Work
CSRF exploits the trust that a web application has in a user’s browser. When a user is authenticated, their browser automatically includes authentication credentials (cookies, HTTP authentication) with every request to that domain.
Attack Scenario
- User logs into
bank.comand receives a session cookie - User visits malicious site
evil.com(without logging out) evil.comcontains code that makes a request tobank.com- Browser automatically includes the user’s authentication cookie
bank.comprocesses the request as if the user intended it
Example Attack
html<!-- On evil.com --> <img src="https://bank.com/api/transfer?to=attacker&amount=10000" /> <!-- Or using a form --> <form action="https://bank.com/api/transfer" method="POST" id="csrf-form"> <input type="hidden" name="to" value="attacker" /> <input type="hidden" name="amount" value="10000" /> </form> <script> document.getElementById('csrf-form').submit(); </script>
Vulnerable Code Example
javascript// VULNERABLE: No CSRF protection const express = require('express'); const session = require('express-session'); const app = express(); app.use(express.urlencoded({ extended: true })); app.use(session({ secret: 'session-secret', cookie: { httpOnly: true } })); // Vulnerable endpoint - no CSRF token validation app.post('/api/transfer', (req, res) => { if (!req.session.userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { to, amount } = req.body; // Process transfer - vulnerable to CSRF! processTransfer(req.session.userId, to, amount); res.json({ success: true }); });
CSRF Protection Methods
1. Synchronizer Token Pattern (Recommended)
javascriptconst express = require('express'); const csrf = require('csurf'); const cookieParser = require('cookie-parser'); const app = express(); app.use(cookieParser()); app.use(express.urlencoded({ extended: true })); app.use(express.json()); // CSRF protection middleware const csrfProtection = csrf({ cookie: { httpOnly: true, secure: true, // HTTPS only sameSite: 'strict' } }); // Apply to all routes that modify data app.use('/api/*', csrfProtection); // Endpoint to get CSRF token app.get('/api/csrf-token', csrfProtection, (req, res) => { res.json({ csrfToken: req.csrfToken() }); }); // Protected endpoint app.post('/api/transfer', csrfProtection, (req, res) => { if (!req.session.userId) { return res.status(401).json({ error: 'Not authenticated' }); } const { to, amount } = req.body; processTransfer(req.session.userId, to, amount); res.json({ success: true }); }); // Error handler for CSRF token errors app.use((err, req, res, next) => { if (err.code === 'EBADCSRFTOKEN') { return res.status(403).json({ error: 'Invalid CSRF token' }); } next(err); });
Frontend Implementation with CSRF Token
javascript// React example import { useState, useEffect } from 'react'; function TransferForm() { const [csrfToken, setCsrfToken] = useState(''); const [formData, setFormData] = useState({ to: '', amount: '' }); // Fetch CSRF token on component mount useEffect(() => { fetch('/api/csrf-token', { credentials: 'include' }) .then(res => res.json()) .then(data => setCsrfToken(data.csrfToken)) .catch(err => console.error('Failed to fetch CSRF token:', err)); }, []); const handleSubmit = async (e) => { e.preventDefault(); try { const response = await fetch('/api/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json', 'CSRF-Token': csrfToken }, credentials: 'include', body: JSON.stringify(formData) }); if (!response.ok) { throw new Error('Transfer failed'); } const result = await response.json(); console.log('Transfer successful:', result); } catch (error) { console.error('Error:', error); } }; return ( <form onSubmit={handleSubmit}> <input type="text" value={formData.to} onChange={(e) => setFormData({ ...formData, to: e.target.value })} placeholder="Recipient" /> <input type="number" value={formData.amount} onChange={(e) => setFormData({ ...formData, amount: e.target.value })} placeholder="Amount" /> <button type="submit">Transfer</button> </form> ); }
2. Double Submit Cookie Pattern
javascriptconst crypto = require('crypto'); function generateCSRFToken() { return crypto.randomBytes(32).toString('hex'); } // Middleware to set CSRF token cookie app.use((req, res, next) => { if (!req.cookies.csrfToken) { const token = generateCSRFToken(); res.cookie('csrfToken', token, { httpOnly: false, // Must be readable by JavaScript secure: true, sameSite: 'strict', maxAge: 3600000 // 1 hour }); } next(); }); // Validation middleware function validateCSRF(req, res, next) { const tokenFromCookie = req.cookies.csrfToken; const tokenFromHeader = req.headers['x-csrf-token']; if (!tokenFromCookie || !tokenFromHeader || tokenFromCookie !== tokenFromHeader) { return res.status(403).json({ error: 'Invalid CSRF token' }); } next(); } // Apply to routes app.post('/api/transfer', validateCSRF, (req, res) => { // Process transfer res.json({ success: true }); });
3. SameSite Cookie Attribute
javascriptconst session = require('express-session'); app.use(session({ secret: process.env.SESSION_SECRET, name: 'sessionId', cookie: { httpOnly: true, secure: true, // HTTPS only sameSite: 'strict', // or 'lax' for less strict protection maxAge: 3600000 }, resave: false, saveUninitialized: false }));
SameSite values:
- Strict: Cookie only sent for same-site requests (best protection, may affect UX)
- Lax: Cookie sent on top-level navigation (good balance)
- None: Cookie sent with all requests (requires Secure flag)
4. Custom Request Headers (for SPAs)
javascript// Middleware to verify custom header function requireCustomHeader(req, res, next) { const customHeader = req.headers['x-requested-with']; if (customHeader !== 'XMLHttpRequest') { return res.status(403).json({ error: 'Missing required header' }); } next(); } // Apply to API routes app.use('/api/*', requireCustomHeader);
javascript// Frontend: Always include custom header fetch('/api/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'include', body: JSON.stringify(data) });
Defense in Depth Approach
javascriptconst express = require('express'); const csrf = require('csurf'); const helmet = require('helmet'); const app = express(); // 1. Security headers app.use(helmet()); // 2. SameSite cookies app.use(session({ secret: process.env.SESSION_SECRET, cookie: { httpOnly: true, secure: true, sameSite: 'strict' } })); // 3. CSRF token validation const csrfProtection = csrf({ cookie: true }); app.use('/api/*', csrfProtection); // 4. Verify Referer/Origin headers function verifyOrigin(req, res, next) { const origin = req.headers.origin || req.headers.referer; if (!origin || !origin.startsWith(process.env.ALLOWED_ORIGIN)) { return res.status(403).json({ error: 'Invalid origin' }); } next(); } app.use('/api/*', verifyOrigin); // 5. Re-authentication for sensitive operations function requireRecentAuth(req, res, next) { const lastAuth = req.session.lastAuthTime; const fiveMinutes = 5 * 60 * 1000; if (!lastAuth || Date.now() - lastAuth > fiveMinutes) { return res.status(403).json({ error: 'Please re-authenticate for this operation' }); } next(); } app.post('/api/transfer', requireRecentAuth, (req, res) => { // Process sensitive operation });
CSRF Protection Checklist
- ✅ Use CSRF tokens for state-changing operations
- ✅ Set SameSite cookie attribute
- ✅ Validate Origin/Referer headers
- ✅ Use custom headers for AJAX requests
- ✅ Implement re-authentication for sensitive operations
- ✅ Use POST/PUT/DELETE for state changes (never GET)
- ✅ Ensure HTTPS everywhere
- ✅ Don’t expose CSRF tokens in URLs
- ✅ Regenerate tokens after authentication
- ✅ Test CSRF protection regularly
Conclusion
CSRF attacks exploit the implicit trust browsers have in authenticated sessions. Effective protection requires multiple layers of defense: CSRF tokens, SameSite cookies, origin validation, and re-authentication for sensitive operations.
Modern frameworks often provide built-in CSRF protection, but it’s crucial to understand how it works and ensure it’s properly configured. Regular security testing helps verify your CSRF defenses are working correctly.
For comprehensive security assessments including CSRF testing, contact the Whitespots team for professional penetration testing services.


