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)
|
* `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)
|
* `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:
|
You can set these options through environment variables:
|
||||||
* `HASTEBIN_ENABLE_CSP` - Enable/disable CSP (true/false)
|
* `HASTEBIN_ENABLE_CSP` - Enable/disable CSP (true/false)
|
||||||
* `HASTEBIN_ENABLE_HSTS` - Enable/disable HSTS (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_BYPASS_CSP_IN_DEV` - Allow unsafe-inline in development (true/false)
|
||||||
* `HASTEBIN_ALLOW_UNSAFE_HASHES` - Allow unsafe-hashes in production (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
|
## Key Generation
|
||||||
|
|
||||||
### Phonetic
|
### Phonetic
|
||||||
|
|
80
lib/csp.js
80
lib/csp.js
|
@ -4,34 +4,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const winston = require('winston');
|
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
|
// CSP middleware
|
||||||
function cspMiddleware(config) {
|
function cspMiddleware(config) {
|
||||||
// Default to enabled if not specified
|
// Default to enabled if not specified
|
||||||
|
@ -43,30 +17,6 @@ function cspMiddleware(config) {
|
||||||
return function(req, res, next) { next(); };
|
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) {
|
return function(req, res, next) {
|
||||||
// Only add CSP headers for HTML requests
|
// Only add CSP headers for HTML requests
|
||||||
const isHtmlRequest = req.url === '/' || req.url.match(/^\/[a-zA-Z0-9_-]+$/);
|
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
|
// Store nonce in request object for use in HTML template
|
||||||
req.cspNonce = nonce;
|
req.cspNonce = nonce;
|
||||||
|
|
||||||
// Build the base script sources list - now only self-hosted content
|
// Build the base script sources list
|
||||||
const baseScriptSources = ["'self'"];
|
const scriptSources = ["'self'", `'nonce-${nonce}'`];
|
||||||
|
|
||||||
// Add nonce
|
// Add any additional script sources from config
|
||||||
baseScriptSources.push(`'nonce-${nonce}'`);
|
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
|
// Add unsafe-hashes if configured (for event handlers)
|
||||||
if (appJsHash) baseScriptSources.push(appJsHash);
|
if (config.security && config.security.allowUnsafeHashes) {
|
||||||
if (highlightJsHash) baseScriptSources.push(highlightJsHash);
|
scriptSources.push("'unsafe-hashes'");
|
||||||
if (jqueryJsHash) baseScriptSources.push(jqueryJsHash);
|
}
|
||||||
|
|
||||||
// Create the policy - adjust based on environment
|
// Create the policy - adjust based on environment
|
||||||
let cspDirectives;
|
let cspDirectives;
|
||||||
|
@ -113,7 +69,7 @@ function cspMiddleware(config) {
|
||||||
// Standard development mode - still using nonces
|
// Standard development mode - still using nonces
|
||||||
cspDirectives = [
|
cspDirectives = [
|
||||||
"default-src 'self'",
|
"default-src 'self'",
|
||||||
`script-src ${baseScriptSources.join(' ')}`,
|
`script-src ${scriptSources.join(' ')}`,
|
||||||
"style-src 'self' 'unsafe-inline'",
|
"style-src 'self' 'unsafe-inline'",
|
||||||
"img-src 'self' data:",
|
"img-src 'self' data:",
|
||||||
"connect-src 'self'",
|
"connect-src 'self'",
|
||||||
|
@ -123,10 +79,10 @@ function cspMiddleware(config) {
|
||||||
winston.debug('Using development CSP policy with nonces (bypass disabled)');
|
winston.debug('Using development CSP policy with nonces (bypass disabled)');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Production mode - always strict policy with nonces and hashes
|
// Production mode - strict policy with nonces
|
||||||
cspDirectives = [
|
cspDirectives = [
|
||||||
"default-src 'self'",
|
"default-src 'self'",
|
||||||
`script-src ${baseScriptSources.join(' ')}${config.security.allowUnsafeHashes ? " 'unsafe-hashes'" : ""}`,
|
`script-src ${scriptSources.join(' ')}`,
|
||||||
"style-src 'self' 'unsafe-inline'",
|
"style-src 'self' 'unsafe-inline'",
|
||||||
"img-src 'self' data:",
|
"img-src 'self' data:",
|
||||||
"connect-src 'self'",
|
"connect-src 'self'",
|
||||||
|
@ -137,7 +93,7 @@ function cspMiddleware(config) {
|
||||||
"object-src 'none'"
|
"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
|
// Set the CSP header with the properly formatted policy
|
||||||
|
|
|
@ -49,7 +49,19 @@ function templateHandlerMiddleware(staticDir) {
|
||||||
// Process the template - replace all nonce placeholders
|
// Process the template - replace all nonce placeholders
|
||||||
let html;
|
let html;
|
||||||
try {
|
try {
|
||||||
|
// Replace {{cspNonce}} placeholders
|
||||||
html = templateCache[indexPath].replace(/\{\{cspNonce\}\}/g, nonce);
|
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) {
|
} catch (err) {
|
||||||
winston.error('Error processing template:', err);
|
winston.error('Error processing template:', err);
|
||||||
return next();
|
return next();
|
||||||
|
|
Loading…
Reference in New Issue