tractatus/src/middleware/csrf-protection.middleware.js
TheFlow 2298d36bed fix(submissions): restructure Economist package and fix article display
- Create Economist SubmissionTracking package correctly:
  * mainArticle = full blog post content
  * coverLetter = 216-word SIR— letter
  * Links to blog post via blogPostId
- Archive 'Letter to The Economist' from blog posts (it's the cover letter)
- Fix date display on article cards (use published_at)
- Target publication already displaying via blue badge

Database changes:
- Make blogPostId optional in SubmissionTracking model
- Economist package ID: 68fa85ae49d4900e7f2ecd83
- Le Monde package ID: 68fa2abd2e6acd5691932150

Next: Enhanced modal with tabs, validation, export

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 08:47:42 +13:00

118 lines
2.9 KiB
JavaScript

/**
* CSRF Protection Middleware (Modern Approach)
*
* Uses SameSite cookies + double-submit cookie pattern
* Replaces deprecated csurf package
*
* Reference: OWASP CSRF Prevention Cheat Sheet
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
*/
const crypto = require('crypto');
const { logSecurityEvent, getClientIp } = require('../utils/security-logger');
/**
* Generate CSRF token
*/
function generateCsrfToken() {
return crypto.randomBytes(32).toString('hex');
}
/**
* CSRF Protection Middleware
*
* Uses double-submit cookie pattern:
* 1. Server sets CSRF token in secure, SameSite cookie
* 2. Client must send same token in custom header (X-CSRF-Token)
* 3. Server validates cookie matches header
*/
function csrfProtection(req, res, next) {
// Skip GET, HEAD, OPTIONS (safe methods)
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
// Get CSRF token from cookie
const cookieToken = req.cookies['csrf-token'];
// Get CSRF token from header
const headerToken = req.headers['x-csrf-token'] || req.headers['csrf-token'];
// Validate tokens exist and match
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
logSecurityEvent({
type: 'csrf_violation',
sourceIp: getClientIp(req),
userId: req.user?.id,
endpoint: req.path,
userAgent: req.get('user-agent'),
details: {
method: req.method,
hasCookie: !!cookieToken,
hasHeader: !!headerToken,
tokensMatch: cookieToken === headerToken
},
action: 'blocked',
severity: 'high'
});
return res.status(403).json({
error: 'Forbidden',
message: 'Invalid CSRF token',
code: 'CSRF_VALIDATION_FAILED'
});
}
next();
}
/**
* Middleware to set CSRF token cookie
* Apply this globally or on routes that need CSRF protection
*/
function setCsrfToken(req, res, next) {
// Only set cookie if it doesn't exist
if (!req.cookies['csrf-token']) {
const token = generateCsrfToken();
//Check if we're behind a proxy (X-Forwarded-Proto header)
const isSecure = req.secure || req.headers['x-forwarded-proto'] === 'https';
res.cookie('csrf-token', token, {
httpOnly: true,
secure: isSecure && process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
});
}
next();
}
/**
* Endpoint to get CSRF token for client-side usage
* GET /api/csrf-token
*
* Returns the CSRF token from the cookie so client can include it in requests
*/
function getCsrfToken(req, res) {
const token = req.cookies['csrf-token'];
if (!token) {
return res.status(400).json({
error: 'Bad Request',
message: 'No CSRF token found. Visit the site first to receive a token.'
});
}
res.json({
csrfToken: token
});
}
module.exports = {
csrfProtection,
setCsrfToken,
getCsrfToken,
generateCsrfToken
};