Understanding CSRF Attacks
Cross-Site Request Forgery (CSRF) tricks users into performing unwanted actions on a site where they're authenticated. The attack exploits the trust a site has in the user's browser.
How CSRF Works
<!-- Attacker's malicious page -->
<html>
<body>
<h1>You won a prize!</h1>
<!-- Hidden form that auto-submits -->
<form action="https://bank.com/transfer" method="POST" id="evil-form">
<input type="hidden" name="to" value="attacker-account">
<input type="hidden" name="amount" value="10000">
</form>
<script>
document.getElementById('evil-form').submit();
</script>
</body>
</html>
<!-- If user is logged into bank.com, this transfer will execute! -->
CSRF Protection Methods
1. CSRF Tokens (Synchronizer Token Pattern)
// Server-side: Generate token
const crypto = require('crypto');
function generateCSRFToken(session) {
const token = crypto.randomBytes(32).toString('hex');
session.csrfToken = token;
return token;
}
// Include token in forms
app.get('/transfer', (req, res) => {
const token = generateCSRFToken(req.session);
res.render('transfer', { csrfToken: token });
});
// Validate token on submission
app.post('/transfer', (req, res) => {
const { csrfToken } = req.body;
if (!csrfToken || csrfToken !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
// Process the transfer
});
<!-- Include token in form -->
<form action="/transfer" method="POST">
<input type="hidden" name="csrfToken" value="{{csrfToken}}">
<input type="text" name="to" placeholder="Recipient">
<input type="number" name="amount" placeholder="Amount">
<button type="submit">Transfer</button>
</form>
2. SameSite Cookies
// Set SameSite attribute on cookies
res.cookie('sessionId', sessionId, {
httpOnly: true, // Not accessible via JavaScript
secure: true, // Only sent over HTTPS
sameSite: 'strict', // Not sent on cross-site requests
maxAge: 3600000, // 1 hour
});
// SameSite options:
// 'strict' - Cookie never sent on cross-site requests
// 'lax' - Cookie sent on top-level navigations (default in modern browsers)
// 'none' - Cookie always sent (requires Secure flag)
3. Double Submit Cookie Pattern
// Set CSRF token in cookie and require it in header
app.use((req, res, next) => {
if (!req.cookies.csrfToken) {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrfToken', token, { sameSite: 'strict' });
}
next();
});
// Client sends token in header
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('csrfToken'),
},
body: JSON.stringify(data),
});
// Server validates
app.post('/api/transfer', (req, res) => {
const cookieToken = req.cookies.csrfToken;
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
// Process request
});
4. Using csurf Middleware (Express)
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.use(csrf({ cookie: true }));
// Token is available as req.csrfToken()
app.get('/form', (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
// Validation happens automatically on POST/PUT/DELETE
5. Next.js CSRF Protection
// lib/csrf.ts
import Tokens from 'csrf';
const tokens = new Tokens();
const secret = process.env.CSRF_SECRET!;
export function generateToken(): string {
return tokens.create(secret);
}
export function verifyToken(token: string): boolean {
return tokens.verify(secret, token);
}
// API route
import { verifyToken } from '@/lib/csrf';
export async function POST(request: Request) {
const token = request.headers.get('x-csrf-token');
if (!token || !verifyToken(token)) {
return Response.json({ error: 'Invalid CSRF token' }, { status: 403 });
}
// Process request
}
// React component
function Form({ csrfToken }) {
const handleSubmit = async (data) => {
await fetch('/api/submit', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify(data),
});
};
return <form onSubmit={handleSubmit}>...</form>;
}
Additional CSRF Mitigations
// Check Origin/Referer headers
function validateOrigin(req) {
const origin = req.headers.origin || req.headers.referer;
const allowedOrigins = ['https://mysite.com'];
if (!origin) return false;
try {
const url = new URL(origin);
return allowedOrigins.includes(url.origin);
} catch {
return false;
}
}
// Require re-authentication for sensitive actions
app.post('/change-password', requireReauth, (req, res) => {
// Only proceed if user recently re-authenticated
});
// Use custom headers (can't be set cross-origin without CORS)
fetch('/api/data', {
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
});
CSRF Checklist
- Use CSRF tokens for all state-changing operations
- Set SameSite=Strict or Lax on session cookies
- Validate Origin/Referer headers as additional check
- Use POST for state-changing operations (never GET)
- Require re-authentication for sensitive actions
- Consider using custom request headers