Enhance CSP implementation with nonces and improve documentation

This commit is contained in:
Leopere 2025-03-01 17:59:07 -05:00
parent 6e7b63a408
commit a88c7c6ccf
3 changed files with 63 additions and 62 deletions

View File

@ -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

View File

@ -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

View File

@ -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 <script> tags without nonce attribute get one
// This is a backup in case some scripts don't have the placeholder
html = html.replace(/<script([^>]*)>/g, function(match, attributes) {
// If it already has a nonce attribute, leave it alone
if (attributes && attributes.includes('nonce=')) {
return match;
}
// Otherwise, add the nonce attribute
return `<script${attributes} nonce="${nonce}">`;
});
} catch (err) {
winston.error('Error processing template:', err);
return next();