Enhance CSP implementation with nonces and improve documentation
This commit is contained in:
parent
6e7b63a408
commit
a88c7c6ccf
33
README.md
33
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
|
||||
|
|
80
lib/csp.js
80
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
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue