Security
CSRF Protection
Prevent cross-site request forgery attacks
Intermediate
security csrf tokens cookies
Definition
Cross-Site Request Forgery (CSRF) is an attack where a malicious website tricks a user’s browser into performing unwanted actions on a trusted site where the user is authenticated. CSRF attacks exploit the fact that browsers automatically send cookies with every request to a domain.
How CSRF Works
The Attack
1. User logs into bank.com
- Receives authentication cookie
2. User visits malicious.com
- Without logging out of bank.com
3. malicious.com contains:
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit()</script>
4. Browser automatically includes bank.com cookie
5. Bank processes transfer as if user initiated it
Protection Methods
1. CSRF Tokens (Synchronizer Token Pattern)
// Server generates token
const csrfToken = crypto.randomBytes(32).toString('hex');
req.session.csrfToken = csrfToken;
// Include in HTML form
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="text" name="to">
<input type="number" name="amount">
<button type="submit">Transfer</button>
</form>
// Server validates
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).send('Invalid CSRF token');
}
// Process transfer
});
2. Double Submit Cookie
// Server sets cookie with random token
res.cookie('csrf_token', randomToken, {
httpOnly: false, // JavaScript can read
secure: true,
sameSite: 'Strict'
});
// JavaScript reads cookie and sends in header
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('csrf_token') // Read from cookie
},
body: JSON.stringify({ to, amount })
});
// Server validates header matches cookie
app.post('/api/transfer', (req, res) => {
if (req.headers['x-csrf-token'] !== req.cookies.csrf_token) {
return res.status(403).send('CSRF validation failed');
}
// Process
});
3. SameSite Cookies (Modern Approach)
// Set-Cookie with SameSite attribute
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'Strict' // or 'Lax'
});
SameSite Options:
Strict: Cookie never sent in cross-site requests
Most secure, but breaks some flows
Lax: Cookie sent for top-level navigation GET requests
Good balance of security and usability
None: Cookie sent with all requests
Must use with Secure attribute
Not recommended without additional CSRF protection
Implementation Examples
Express.js with csurf
const csurf = require('csurf');
// Setup middleware
const csrfProtection = csurf({
cookie: {
httpOnly: true,
secure: true,
sameSite: 'Strict'
}
});
// Apply to routes
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/submit', csrfProtection, (req, res) => {
// Token automatically validated
res.send('Success');
});
// Error handler
app.use((err, req, res, next) => {
if (err.code !== 'EBADCSRFTOKEN') return next(err);
res.status(403).send('Invalid CSRF token');
});
React + API
// Custom hook for CSRF token
function useCSRFToken() {
const [token, setToken] = useState(null);
useEffect(() => {
fetch('/api/csrf-token')
.then(res => res.json())
.then(data => setToken(data.token));
}, []);
return token;
}
// Include in requests
function TransferForm() {
const csrfToken = useCSRFToken();
const handleSubmit = async (e) => {
e.preventDefault();
await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
credentials: 'include', // Send cookies
body: JSON.stringify({ to, amount })
});
};
return <form onSubmit={handleSubmit}>...</form>;
}
Best Practices
1. SameSite=Lax as Baseline
// Modern browsers support SameSite
// Lax protects against POST CSRF while allowing normal navigation
app.use(cookieSession({
name: 'session',
httpOnly: true,
secure: true,
sameSite: 'lax' // Default in modern browsers
}));
2. Additional Token for Sensitive Actions
// Even with SameSite, use tokens for:
// - State-changing operations
// - Financial transactions
// - Account modifications
// SameSite doesn't protect against:
// - Subdomain attacks (if not Strict)
// - XSS (use httpOnly cookies)
3. Validate Origin/Referer
// Additional validation
app.post('/sensitive', (req, res) => {
const origin = req.headers.origin;
const referer = req.headers.referer;
if (!origin || !origin.includes('trusted-domain.com')) {
return res.status(403).send('Invalid origin');
}
// Also check CSRF token
// ...
});
4. Custom Request Headers
// AJAX requests with custom header
fetch('/api/action', {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
// Simple requests (form posts) can't set custom headers
// So attackers can't forge these requests cross-origin
Common Mistakes
// ❌ GET requests that modify state
app.get('/delete-account', (req, res) => {
// Dangerous! Can be triggered by <img> tag
});
// ✅ Use POST/PUT/DELETE for actions
app.post('/delete-account', (req, res) => {
// Protected by CORS and CSRF measures
});
// ❌ Missing CSRF protection on state changes
app.post('/change-email', (req, res) => {
// No token check!
});
// ❌ Predictable tokens
const token = user.id + timestamp; // Easy to guess
// ✅ Cryptographically secure tokens
const token = crypto.randomBytes(32).toString('hex');
Testing CSRF Protection
// Attempt CSRF attack in tests
describe('CSRF Protection', () => {
it('should reject requests without token', async () => {
const response = await request(app)
.post('/api/transfer')
.send({ to: 'attacker', amount: 1000 })
.set('Cookie', 'session=valid-session');
expect(response.status).toBe(403);
});
it('should accept requests with valid token', async () => {
// Get token first
const tokenRes = await request(app).get('/api/csrf-token');
const token = tokenRes.body.token;
const response = await request(app)
.post('/api/transfer')
.send({ to: 'friend', amount: 100 })
.set('X-CSRF-Token', token)
.set('Cookie', 'session=valid-session');
expect(response.status).toBe(200);
});
});
Key Takeaway
Protect against CSRF using SameSite=Lax cookies as baseline, add CSRF tokens for sensitive actions, validate Origin/Referer headers, and use POST for state-changing operations. Modern browsers with SameSite=Lax provide good default protection, but defense in depth is recommended for critical applications.