- 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>
118 lines
2.9 KiB
JavaScript
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
|
|
};
|