Cross-Site Request Forgery (CSRF) is when someone tricks your users into making actions they didn’t intend to make. Let’s see how to stop this.
Quick Implementation Guide
1. Install Required Packages
# For Node.js/Express
npm install csurf cookie-parser
# For Django
pip install django-csrf-protection
# For Laravel
composer require laravel/sanctum
2. Express.js Implementation
const express = require('express');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const app = express();
// Setup CSRF protection
app.use(cookieParser());
app.use(csrf({ cookie: true }));
// Add CSRF token to all responses
app.use((req, res, next) => {
res.locals.csrfToken = req.csrfToken();
next();
});
// Example protected route
app.post('/transfer-money', (req, res) => {
// CSRF token is automatically validated
const { amount, toAccount } = req.body;
// Process transfer...
res.json({ success: true });
});
3. Frontend Implementation
// Add this to your form
<form action="/transfer-money" method="POST">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<input type="number" name="amount">
<input type="text" name="toAccount">
<button type="submit">Transfer Money</button>
</form>
// For AJAX requests
fetch('/transfer-money', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ amount: 100, toAccount: '12345' })
});
Advanced Protection
1. Double Submit Cookie Pattern
// Server-side
app.use((req, res, next) => {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrf-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
});
res.locals.csrfToken = token;
next();
});
// Client-side
document.cookie = `csrf-token=${token}; path=/; secure; samesite=strict`;
2. Custom CSRF Middleware
const csrfProtection = (req, res, next) => {
const token = req.headers['x-csrf-token'];
const cookieToken = req.cookies['csrf-token'];
if (!token || !cookieToken || token !== cookieToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
};
app.post('/sensitive-action', csrfProtection, (req, res) => {
// Your protected code here
});
Important Notes
- Always use HTTPS in production
- Set appropriate SameSite cookie attributes
- Consider using state-changing tokens for extra security
- Implement proper error handling for CSRF failures
Security Checklist
- ✅ All forms include CSRF tokens
- ✅ AJAX requests include CSRF headers
- ✅ Cookies are properly configured
- ✅ Error messages don’t leak sensitive information
- ✅ Regular security audits are performed
Testing Your Implementation
// Test script
const testCSRF = async () => {
// Try without token
const response1 = await fetch('/transfer-money', {
method: 'POST',
body: JSON.stringify({ amount: 100 })
});
console.log('Without token:', response1.status); // Should be 403
// Try with invalid token
const response2 = await fetch('/transfer-money', {
method: 'POST',
headers: { 'X-CSRF-Token': 'invalid' },
body: JSON.stringify({ amount: 100 })
});
console.log('With invalid token:', response2.status); // Should be 403
// Try with valid token
const token = document.cookie.match(/csrf-token=([^;]+)/)[1];
const response3 = await fetch('/transfer-money', {
method: 'POST',
headers: { 'X-CSRF-Token': token },
body: JSON.stringify({ amount: 100 })
});
console.log('With valid token:', response3.status); // Should be 200
};
Common Issues and Solutions
// Issue: CSRF token mismatch
// Solution: Ensure token is properly passed
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
res.status(403).json({ error: 'Session expired. Please refresh the page.' });
} else {
next(err);
}
});
// Issue: Token not available in AJAX requests
// Solution: Add meta tag
<meta name="csrf-token" content="<%= csrfToken %>">
// Issue: Cookie not being set
// Solution: Check cookie configuration
app.use(csrf({
cookie: {
key: '_csrf',
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'strict'
}
}));