Understanding and Preventing CSRF Attacks

Understanding and Preventing CSRF Attacks

Whitespots Team ·
csrf
web
authentication

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

  1. User logs into bank.com and receives a session cookie
  2. User visits malicious site evil.com (without logging out)
  3. evil.com contains code that makes a request to bank.com
  4. Browser automatically includes the user’s authentication cookie
  5. bank.com processes 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

javascript
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 })); 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> ); }
javascript
const 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 }); });
javascript
const 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

javascript
const 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.