Table Of Contents
Quick Fix: Basic CSRF Protection
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
// Basic CSRF middleware setup
const express = require('express');
const app = express();
app.use(cookieParser());
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
// Configure CSRF protection
const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
}
});
// Apply CSRF protection to forms
app.use('/forms', csrfProtection);
// Generate CSRF token for forms
app.get('/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// Protected form endpoint
app.post('/forms/submit', csrfProtection, (req, res) => {
res.json({
message: 'Form submitted successfully',
data: req.body
});
});
// Custom error handler for CSRF
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
res.status(403).json({
error: 'Invalid CSRF token',
code: 'CSRF_TOKEN_INVALID'
});
} else {
next(err);
}
});
The Problem: Comprehensive CSRF Protection System
const crypto = require('crypto');
const cookieParser = require('cookie-parser');
// Advanced CSRF protection service
class CSRFProtectionService {
constructor(options = {}) {
this.secret = options.secret || process.env.CSRF_SECRET || crypto.randomBytes(32).toString('hex');
this.tokenLength = options.tokenLength || 32;
this.saltLength = options.saltLength || 16;
this.cookieName = options.cookieName || 'csrfToken';
this.headerName = options.headerName || 'x-csrf-token';
this.formFieldName = options.formFieldName || '_csrf';
this.maxAge = options.maxAge || 60 * 60 * 1000; // 1 hour
this.secure = options.secure !== false && process.env.NODE_ENV === 'production';
this.sameSite = options.sameSite || 'strict';
this.httpOnly = options.httpOnly !== false;
this.skipMethods = options.skipMethods || ['GET', 'HEAD', 'OPTIONS'];
this.trustedOrigins = options.trustedOrigins || [];
this.enableDoubleSubmit = options.enableDoubleSubmit !== false;
this.enableOriginCheck = options.enableOriginCheck !== false;
this.enableRefererCheck = options.enableRefererCheck !== false;
}
// Generate CSRF token
generateToken(sessionId = null) {
const salt = crypto.randomBytes(this.saltLength).toString('hex');
const hash = this.generateHash(salt, sessionId);
return {
token: `${salt}:${hash}`,
salt,
hash
};
}
// Generate hash for token validation
generateHash(salt, sessionId = null) {
const data = sessionId ? `${salt}:${sessionId}` : salt;
return crypto
.createHmac('sha256', this.secret)
.update(data)
.digest('hex');
}
// Verify CSRF token
verifyToken(token, sessionId = null) {
if (!token || typeof token !== 'string') {
return false;
}
const parts = token.split(':');
if (parts.length !== 2) {
return false;
}
const [salt, hash] = parts;
const expectedHash = this.generateHash(salt, sessionId);
// Use constant-time comparison to prevent timing attacks
return this.constantTimeCompare(hash, expectedHash);
}
// Constant-time string comparison
constantTimeCompare(a, b) {
if (a.length !== b.length) {
return false;
}
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
// Double submit cookie protection
generateDoubleSubmitToken() {
return crypto.randomBytes(this.tokenLength).toString('hex');
}
// Verify double submit token
verifyDoubleSubmitToken(cookieToken, headerToken) {
if (!cookieToken || !headerToken) {
return false;
}
return this.constantTimeCompare(cookieToken, headerToken);
}
// Create comprehensive CSRF middleware
createMiddleware(options = {}) {
const {
ignorePaths = [],
customValidator = null,
onViolation = null,
useSession = true
} = options;
return (req, res, next) => {
try {
// Skip CSRF protection for certain methods
if (this.skipMethods.includes(req.method)) {
return next();
}
// Skip for ignored paths
if (ignorePaths.some(path => req.path.startsWith(path))) {
return next();
}
// Origin validation
if (this.enableOriginCheck && !this.validateOrigin(req)) {
this.handleViolation(req, res, 'Invalid origin', 'INVALID_ORIGIN', onViolation);
return;
}
// Referer validation
if (this.enableRefererCheck && !this.validateReferer(req)) {
this.handleViolation(req, res, 'Invalid referer', 'INVALID_REFERER', onViolation);
return;
}
// Get session ID if using sessions
const sessionId = useSession ? req.sessionID || req.session?.id : null;
// Double submit cookie protection
if (this.enableDoubleSubmit) {
const cookieToken = req.cookies[this.cookieName];
const headerToken = req.headers[this.headerName] || req.body[this.formFieldName];
if (!this.verifyDoubleSubmitToken(cookieToken, headerToken)) {
this.handleViolation(req, res, 'Invalid CSRF token', 'CSRF_TOKEN_MISMATCH', onViolation);
return;
}
} else {
// Traditional CSRF token validation
const token = req.headers[this.headerName] || req.body[this.formFieldName];
if (!this.verifyToken(token, sessionId)) {
this.handleViolation(req, res, 'Invalid CSRF token', 'CSRF_TOKEN_INVALID', onViolation);
return;
}
}
// Custom validation
if (customValidator && !customValidator(req)) {
this.handleViolation(req, res, 'Custom CSRF validation failed', 'CUSTOM_VALIDATION_FAILED', onViolation);
return;
}
next();
} catch (error) {
console.error('CSRF middleware error:', error);
res.status(500).json({
error: 'CSRF validation failed',
code: 'CSRF_ERROR'
});
}
};
}
// Token generation middleware
createTokenMiddleware(options = {}) {
const { useSession = true, autoSet = true } = options;
return (req, res, next) => {
try {
const sessionId = useSession ? req.sessionID || req.session?.id : null;
if (this.enableDoubleSubmit) {
// Generate double submit token
const token = this.generateDoubleSubmitToken();
if (autoSet) {
res.cookie(this.cookieName, token, {
httpOnly: false, // Must be accessible to JavaScript
secure: this.secure,
sameSite: this.sameSite,
maxAge: this.maxAge
});
}
req.csrfToken = () => token;
} else {
// Generate traditional CSRF token
const tokenData = this.generateToken(sessionId);
if (autoSet) {
res.cookie(this.cookieName, tokenData.token, {
httpOnly: this.httpOnly,
secure: this.secure,
sameSite: this.sameSite,
maxAge: this.maxAge
});
}
req.csrfToken = () => tokenData.token;
}
next();
} catch (error) {
console.error('CSRF token middleware error:', error);
next(error);
}
};
}
// Validate request origin
validateOrigin(req) {
const origin = req.headers.origin;
const host = req.headers.host;
if (!origin) {
// For same-origin requests, origin might not be present
return true;
}
// Check if origin matches host
const originHost = new URL(origin).host;
if (originHost === host) {
return true;
}
// Check trusted origins
return this.trustedOrigins.some(trustedOrigin => {
if (typeof trustedOrigin === 'string') {
return origin === trustedOrigin;
}
if (trustedOrigin instanceof RegExp) {
return trustedOrigin.test(origin);
}
return false;
});
}
// Validate request referer
validateReferer(req) {
const referer = req.headers.referer;
const host = req.headers.host;
if (!referer) {
// Some legitimate requests might not have referer
return true;
}
try {
const refererHost = new URL(referer).host;
return refererHost === host;
} catch (error) {
return false;
}
}
// Handle CSRF violations
handleViolation(req, res, message, code, onViolation) {
const violationData = {
ip: req.ip,
userAgent: req.headers['user-agent'],
path: req.path,
method: req.method,
headers: req.headers,
timestamp: new Date(),
code
};
// Log violation
console.warn('CSRF violation detected:', violationData);
// Custom violation handler
if (onViolation) {
onViolation(violationData, req, res);
}
// Send response
if (req.accepts('json')) {
res.status(403).json({
error: message,
code
});
} else {
res.status(403).send(`
<!DOCTYPE html>
<html>
<head><title>Security Error</title></head>
<body>
<h1>Security Error</h1>
<p>Your request could not be processed due to security restrictions.</p>
<p>Error Code: ${code}</p>
</body>
</html>
`);
}
}
}
// SameSite cookie configuration helper
class SameSiteCookieHelper {
static configureSameSite(app, options = {}) {
const {
defaultSameSite = 'strict',
secureCookies = process.env.NODE_ENV === 'production',
cookiePrefix = '__Host-'
} = options;
// Override Express cookie settings
app.use((req, res, next) => {
const originalCookie = res.cookie;
res.cookie = function(name, value, options = {}) {
// Apply security defaults
const secureOptions = {
httpOnly: options.httpOnly !== false,
secure: options.secure !== false && secureCookies,
sameSite: options.sameSite || defaultSameSite,
...options
};
// Add secure prefix for production
if (secureCookies && secureOptions.secure && secureOptions.httpOnly) {
name = name.startsWith(cookiePrefix) ? name : cookiePrefix + name;
}
return originalCookie.call(this, name, value, secureOptions);
};
next();
});
}
}
// Form CSRF helper for rendering templates
class CSRFFormHelper {
static createHiddenInput(token) {
return `<input type="hidden" name="_csrf" value="${token}">`;
}
static createMetaTag(token) {
return `<meta name="csrf-token" content="${token}">`;
}
static getClientSideScript() {
return `
<script>
// CSRF token handling for AJAX requests
(function() {
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
if (token) {
// Add to all fetch requests
const originalFetch = window.fetch;
window.fetch = function(url, options = {}) {
options.headers = options.headers || {};
options.headers['X-CSRF-Token'] = token;
return originalFetch(url, options);
};
// Add to jQuery AJAX if available
if (window.jQuery) {
jQuery.ajaxSetup({
beforeSend: function(xhr) {
xhr.setRequestHeader('X-CSRF-Token', token);
}
});
}
}
})();
</script>
`;
}
}
// Express application with comprehensive CSRF protection
const express = require('express');
const session = require('express-session');
const app = express();
// Initialize CSRF protection service
const csrfService = new CSRFProtectionService({
enableDoubleSubmit: true,
enableOriginCheck: true,
enableRefererCheck: true,
trustedOrigins: ['https://trusted-domain.com'],
maxAge: 60 * 60 * 1000 // 1 hour
});
// Configure SameSite cookies
SameSiteCookieHelper.configureSameSite(app, {
defaultSameSite: 'strict',
secureCookies: process.env.NODE_ENV === 'production'
});
// Basic middleware
app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Session configuration
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// Generate CSRF token for all requests
app.use(csrfService.createTokenMiddleware({ autoSet: true }));
// CSRF token endpoint
app.get('/csrf-token', (req, res) => {
res.json({
csrfToken: req.csrfToken(),
expires: new Date(Date.now() + csrfService.maxAge).toISOString()
});
});
// Form page with CSRF protection
app.get('/form', (req, res) => {
const token = req.csrfToken();
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>CSRF Protected Form</title>
${CSRFFormHelper.createMetaTag(token)}
</head>
<body>
<h1>Protected Form</h1>
<form method="POST" action="/api/submit">
${CSRFFormHelper.createHiddenInput(token)}
<input type="text" name="message" placeholder="Enter message" required>
<button type="submit">Submit</button>
</form>
<h2>AJAX Example</h2>
<button onclick="submitAjax()">Submit via AJAX</button>
${CSRFFormHelper.getClientSideScript()}
<script>
function submitAjax() {
fetch('/api/ajax-submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ message: 'AJAX test' })
})
.then(response => response.json())
.then(data => alert('AJAX success: ' + data.message))
.catch(error => alert('Error: ' + error.message));
}
</script>
</body>
</html>
`);
});
// API routes with CSRF protection
app.use('/api', csrfService.createMiddleware({
ignorePaths: ['/api/public'],
onViolation: (violationData, req, res) => {
// Custom violation handling
console.error('CSRF violation from:', violationData.ip);
// Optional: Rate limit the IP or block temporarily
// rateLimit.blockIP(violationData.ip);
}
}));
// Protected form submission
app.post('/api/submit', (req, res) => {
res.json({
message: 'Form submitted successfully',
data: req.body,
timestamp: new Date().toISOString()
});
});
// Protected AJAX endpoint
app.post('/api/ajax-submit', (req, res) => {
res.json({
message: 'AJAX request processed successfully',
data: req.body
});
});
// Public endpoint (no CSRF protection)
app.post('/api/public/webhook', (req, res) => {
res.json({ message: 'Webhook processed' });
});
// File upload with CSRF protection
app.post('/api/upload', (req, res) => {
res.json({ message: 'File upload successful' });
});
// API endpoint requiring custom validation
app.post('/api/sensitive', csrfService.createMiddleware({
customValidator: (req) => {
// Additional validation logic
return req.headers['x-api-key'] === process.env.API_KEY;
}
}), (req, res) => {
res.json({ message: 'Sensitive operation completed' });
});
// Error handler
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
res.status(403).json({
error: 'Invalid CSRF token',
code: 'CSRF_TOKEN_INVALID'
});
} else {
console.error('Server error:', err);
res.status(500).json({
error: 'Internal server error',
code: 'INTERNAL_ERROR'
});
}
});
// Health check (no CSRF needed)
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
module.exports = {
CSRFProtectionService,
SameSiteCookieHelper,
CSRFFormHelper,
app
};
CSRF protection solves "cross-site request forgery", "unauthorized state changes", and "session riding" issues. Implement token-based validation, double submit cookies, origin verification. Use SameSite attributes, validate referer headers, handle violations securely. Support both form submissions and AJAX requests with proper error handling. Alternative: SameSite cookies only, custom headers, framework-specific CSRF libraries.
Share this article
Add Comment
No comments yet. Be the first to comment!