From a88c7c6ccf8cff2c261902c45033c7e250218706 Mon Sep 17 00:00:00 2001 From: Leopere Date: Sat, 1 Mar 2025 17:59:07 -0500 Subject: [PATCH] Enhance CSP implementation with nonces and improve documentation --- README.md | 33 +++++++++++++++++ lib/csp.js | 80 ++++++++++------------------------------- lib/template_handler.js | 12 +++++++ 3 files changed, 63 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 3251aeb..d44113a 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ The `security` section in the configuration allows you to control various securi * `bypassCSPInDev` - In development mode (NODE_ENV=development), use a more permissive CSP that includes 'unsafe-inline' (default: false) * `allowUnsafeHashes` - Allow 'unsafe-hashes' in production mode for DOM event handlers (default: true) +### Environment Variables for Security Settings + You can set these options through environment variables: * `HASTEBIN_ENABLE_CSP` - Enable/disable CSP (true/false) * `HASTEBIN_ENABLE_HSTS` - Enable/disable HSTS (true/false) @@ -95,6 +97,37 @@ You can set these options through environment variables: * `HASTEBIN_BYPASS_CSP_IN_DEV` - Allow unsafe-inline in development (true/false) * `HASTEBIN_ALLOW_UNSAFE_HASHES` - Allow unsafe-hashes in production (true/false) +### CSP Implementation Details + +The Content Security Policy implementation in Hastebin uses nonces to secure inline scripts while maintaining functionality: + +1. **Nonces**: A unique cryptographic nonce is generated for each request and applied to all script tags +2. **Development Mode**: When running with `NODE_ENV=development`, you can bypass strict CSP checks using the `bypassCSPInDev` option +3. **Production Mode**: In production, the CSP is configured to use nonces for all scripts, with optional 'unsafe-hashes' for event handlers +4. **Templates**: The template system automatically injects nonces into script tags, so you don't need to manually add them to the HTML + +#### Running in Development Mode + +To run Hastebin with a more permissive CSP for development: + +```bash +NODE_ENV=development HASTEBIN_BYPASS_CSP_IN_DEV=true node server.js +``` + +#### Running in Production Mode + +For production with strict CSP: + +```bash +NODE_ENV=production node server.js +``` + +The CSP implementation ensures that: +- All script sources are properly controlled +- Inline scripts are secured with nonces +- DOM events are properly handled with 'unsafe-hashes' when necessary +- HSTS can be enabled for HTTPS environments + ## Key Generation ### Phonetic diff --git a/lib/csp.js b/lib/csp.js index 99a5b3f..95fae84 100644 --- a/lib/csp.js +++ b/lib/csp.js @@ -4,34 +4,8 @@ */ const crypto = require('crypto'); -const fs = require('fs'); -const path = require('path'); const winston = require('winston'); -// Keep a cache of file hashes -const fileHashes = {}; - -// Generate a SHA-256 hash for a file or string -function generateHash(content) { - const hash = crypto.createHash('sha256'); - hash.update(content); - return `'sha256-${hash.digest('base64')}'`; -} - -// Calculate hash for a file and cache it -function getFileHash(filePath) { - if (!fileHashes[filePath]) { - try { - const content = fs.readFileSync(filePath, 'utf8'); - fileHashes[filePath] = generateHash(content); - } catch (err) { - console.error(`Error generating hash for ${filePath}:`, err); - return null; - } - } - return fileHashes[filePath]; -} - // CSP middleware function cspMiddleware(config) { // Default to enabled if not specified @@ -43,30 +17,6 @@ function cspMiddleware(config) { return function(req, res, next) { next(); }; } - // Calculate hashes for static JS files once at startup - const staticDir = path.join(process.cwd(), 'static'); - let appJsHash, highlightJsHash, jqueryJsHash; - - try { - // Only generate hashes if the files exist - if (fs.existsSync(path.join(staticDir, 'application.min.js'))) { - appJsHash = getFileHash(path.join(staticDir, 'application.min.js')); - winston.debug('Generated hash for application.min.js'); - } - - if (fs.existsSync(path.join(staticDir, 'highlight.min.js'))) { - highlightJsHash = getFileHash(path.join(staticDir, 'highlight.min.js')); - winston.debug('Generated hash for highlight.min.js'); - } - - if (fs.existsSync(path.join(staticDir, 'jquery.min.js'))) { - jqueryJsHash = getFileHash(path.join(staticDir, 'jquery.min.js')); - winston.debug('Generated hash for jquery.min.js'); - } - } catch (err) { - winston.error('Error generating file hashes:', err); - } - return function(req, res, next) { // Only add CSP headers for HTML requests const isHtmlRequest = req.url === '/' || req.url.match(/^\/[a-zA-Z0-9_-]+$/); @@ -78,16 +28,22 @@ function cspMiddleware(config) { // Store nonce in request object for use in HTML template req.cspNonce = nonce; - // Build the base script sources list - now only self-hosted content - const baseScriptSources = ["'self'"]; + // Build the base script sources list + const scriptSources = ["'self'", `'nonce-${nonce}'`]; - // Add nonce - baseScriptSources.push(`'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 static file hashes if available - if (appJsHash) baseScriptSources.push(appJsHash); - if (highlightJsHash) baseScriptSources.push(highlightJsHash); - if (jqueryJsHash) baseScriptSources.push(jqueryJsHash); + // 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; @@ -113,7 +69,7 @@ function cspMiddleware(config) { // Standard development mode - still using nonces cspDirectives = [ "default-src 'self'", - `script-src ${baseScriptSources.join(' ')}`, + `script-src ${scriptSources.join(' ')}`, "style-src 'self' 'unsafe-inline'", "img-src 'self' data:", "connect-src 'self'", @@ -123,10 +79,10 @@ function cspMiddleware(config) { winston.debug('Using development CSP policy with nonces (bypass disabled)'); } } else { - // Production mode - always strict policy with nonces and hashes + // Production mode - strict policy with nonces cspDirectives = [ "default-src 'self'", - `script-src ${baseScriptSources.join(' ')}${config.security.allowUnsafeHashes ? " 'unsafe-hashes'" : ""}`, + `script-src ${scriptSources.join(' ')}`, "style-src 'self' 'unsafe-inline'", "img-src 'self' data:", "connect-src 'self'", @@ -137,7 +93,7 @@ function cspMiddleware(config) { "object-src 'none'" ]; - winston.debug(`Using strict production CSP policy with nonces, hashes${config.security.allowUnsafeHashes ? ", and unsafe-hashes" : ""}`); + winston.debug(`Using strict production CSP policy with nonces${config.security.allowUnsafeHashes ? " and unsafe-hashes" : ""}`); } // Set the CSP header with the properly formatted policy diff --git a/lib/template_handler.js b/lib/template_handler.js index 0bfa8a5..a19e117 100644 --- a/lib/template_handler.js +++ b/lib/template_handler.js @@ -49,7 +49,19 @@ function templateHandlerMiddleware(staticDir) { // Process the template - replace all nonce placeholders let html; try { + // Replace {{cspNonce}} placeholders html = templateCache[indexPath].replace(/\{\{cspNonce\}\}/g, nonce); + + // Also ensure any