156 lines
6.2 KiB
JavaScript
156 lines
6.2 KiB
JavaScript
/**
|
|
* Content Security Policy middleware
|
|
* Generates nonces for inline scripts and sets appropriate CSP headers
|
|
*/
|
|
|
|
const crypto = require('crypto');
|
|
const winston = require('winston');
|
|
|
|
// Security headers middleware (renamed from CSP middleware as it now handles more headers)
|
|
function securityMiddleware(config) {
|
|
// If security is entirely disabled, return a no-op middleware
|
|
if (config.security === false) {
|
|
return function(req, res, next) { next(); };
|
|
}
|
|
|
|
// Get CSP configuration - default to enabled if not specified
|
|
const cspEnabled = config.security && typeof config.security.csp !== 'undefined' ?
|
|
config.security.csp : true;
|
|
|
|
// Log the CSP configuration at startup for debugging
|
|
winston.info(`Security middleware initialized with CSP ${cspEnabled ? 'ENABLED' : 'DISABLED'}`);
|
|
|
|
return function(req, res, next) {
|
|
// Only add security headers for HTML requests
|
|
const isHtmlRequest = req.url === '/' || req.url.match(/^\/[a-zA-Z0-9_-]+$/);
|
|
|
|
if (isHtmlRequest) {
|
|
// Add basic security headers - always applied regardless of CSP setting
|
|
|
|
// 1. X-Content-Type-Options - prevents MIME-type sniffing
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
|
|
// 2. X-Frame-Options - prevents clickjacking
|
|
res.setHeader('X-Frame-Options', 'DENY');
|
|
|
|
// 3. X-XSS-Protection - legacy header, still used by some browsers
|
|
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
|
|
// 4. Referrer-Policy - controls how much referrer information is included
|
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
|
|
// 5. Permissions-Policy - controls browser features
|
|
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');
|
|
|
|
// Apply CSP headers ONLY if explicitly enabled
|
|
if (cspEnabled === true) {
|
|
// Generate a unique nonce for this request
|
|
const nonce = crypto.randomBytes(16).toString('base64');
|
|
|
|
// Store nonce in request object for use in HTML template
|
|
req.cspNonce = nonce;
|
|
|
|
// Build the base script sources list
|
|
const scriptSources = ["'self'", `'nonce-${nonce}'`];
|
|
|
|
// Add any additional script sources from config
|
|
if (config.security && config.security.scriptSources && config.security.scriptSources.length > 0) {
|
|
config.security.scriptSources.forEach(source => {
|
|
if (source && !scriptSources.includes(source)) {
|
|
scriptSources.push(source);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add unsafe-hashes if configured (for event handlers)
|
|
if (config.security && config.security.allowUnsafeHashes) {
|
|
scriptSources.push("'unsafe-hashes'");
|
|
}
|
|
|
|
// Create the policy - adjust based on environment
|
|
let cspDirectives;
|
|
|
|
// Development mode with potential bypass option
|
|
if (process.env.NODE_ENV === 'development') {
|
|
// Check if we should bypass strict CSP in development
|
|
const bypassCSPInDev = config.security && config.security.bypassCSPInDev;
|
|
|
|
if (bypassCSPInDev) {
|
|
// Very permissive policy with unsafe-inline for development testing
|
|
cspDirectives = [
|
|
"default-src 'self'",
|
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
|
"style-src 'self' 'unsafe-inline'",
|
|
"img-src 'self' data:",
|
|
"connect-src 'self'",
|
|
"font-src 'self'"
|
|
];
|
|
|
|
winston.debug('Using permissive development CSP policy with unsafe-inline (bypass enabled)');
|
|
} else {
|
|
// Standard development mode - still using nonces
|
|
cspDirectives = [
|
|
"default-src 'self'",
|
|
`script-src ${scriptSources.join(' ')}`,
|
|
"style-src 'self' 'unsafe-inline'",
|
|
"img-src 'self' data:",
|
|
"connect-src 'self'",
|
|
"font-src 'self'"
|
|
];
|
|
|
|
winston.debug('Using development CSP policy with nonces (bypass disabled)');
|
|
}
|
|
} else {
|
|
// Production mode - strict policy with nonces
|
|
cspDirectives = [
|
|
"default-src 'self'",
|
|
`script-src ${scriptSources.join(' ')}`,
|
|
"style-src 'self' 'unsafe-inline'",
|
|
"img-src 'self' data:",
|
|
"connect-src 'self'",
|
|
"font-src 'self'",
|
|
"frame-ancestors 'none'",
|
|
"base-uri 'self'",
|
|
"form-action 'none'",
|
|
"object-src 'none'"
|
|
];
|
|
|
|
winston.debug(`Using strict production CSP policy with nonces${config.security.allowUnsafeHashes ? " and unsafe-hashes" : ""}`);
|
|
}
|
|
|
|
// Set the CSP header with the properly formatted policy
|
|
res.setHeader('Content-Security-Policy', cspDirectives.join('; '));
|
|
winston.debug('CSP header applied');
|
|
} else {
|
|
winston.debug('CSP is disabled by configuration - not applying CSP header');
|
|
}
|
|
|
|
// Add Cross-Origin headers only if enabled in config
|
|
// These can be problematic for some applications, so we make them optional
|
|
const enableCrossOriginIsolation = config.security && config.security.enableCrossOriginIsolation;
|
|
|
|
if (enableCrossOriginIsolation) {
|
|
// 6. Cross-Origin-Embedder-Policy - for cross-origin isolation
|
|
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
|
|
|
|
// 7. Cross-Origin-Resource-Policy - protects resources from unauthorized requests
|
|
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
|
|
|
|
// 8. Cross-Origin-Opener-Policy - helps with cross-origin isolation
|
|
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
|
|
|
winston.debug('Added Cross-Origin isolation headers');
|
|
}
|
|
|
|
// If configured, add HSTS header
|
|
if (config.security && config.security.hsts) {
|
|
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
|
|
}
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
// Export the middleware
|
|
module.exports = securityMiddleware;
|