hastebin/lib/csp.js

119 lines
4.1 KiB
JavaScript

/**
* Content Security Policy middleware
* Generates nonces for inline scripts and sets appropriate CSP headers
*/
const crypto = require('crypto');
const winston = require('winston');
// CSP middleware
function cspMiddleware(config) {
// Default to enabled if not specified
const enabled = config.security && typeof config.security.csp !== 'undefined' ?
config.security.csp : true;
// If CSP is disabled, return a no-op middleware
if (!enabled) {
return function(req, res, next) { next(); };
}
return function(req, res, next) {
// Only add CSP headers for HTML requests
const isHtmlRequest = req.url === '/' || req.url.match(/^\/[a-zA-Z0-9_-]+$/);
if (isHtmlRequest) {
// 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('; '));
// Add other security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'no-referrer');
// 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 = cspMiddleware;