Add comprehensive security headers support with testing framework

This commit is contained in:
Leopere 2025-03-01 18:33:46 -05:00
parent c0502bc1a4
commit 68afb6fc6c
7 changed files with 205 additions and 78 deletions

View File

@ -75,7 +75,8 @@ The `security` section in the configuration allows you to control various securi
"hsts": false, // Enable HTTP Strict Transport Security
"scriptSources": [], // Additional allowed script sources
"bypassCSPInDev": false, // Use permissive CSP in development mode
"allowUnsafeHashes": true // Allow 'unsafe-hashes' in production for event handlers
"allowUnsafeHashes": true, // Allow 'unsafe-hashes' in production for event handlers
"enableCrossOriginIsolation": false // Enable strict Cross-Origin isolation headers
}
}
```
@ -87,6 +88,7 @@ The `security` section in the configuration allows you to control various securi
* `scriptSources` - Additional script sources to allow - comma-separated list in env vars
* `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)
* `enableCrossOriginIsolation` - Enable strict Cross-Origin isolation headers (COEP, COOP, CORP) which enhance security but may break certain integrations (default: false)
### Environment Variables for Security Settings
@ -96,6 +98,7 @@ You can set these options through environment variables:
* `HASTEBIN_SCRIPT_SOURCES` - Additional script sources (comma-separated)
* `HASTEBIN_BYPASS_CSP_IN_DEV` - Allow unsafe-inline in development (true/false)
* `HASTEBIN_ALLOW_UNSAFE_HASHES` - Allow unsafe-hashes in production (true/false)
* `HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION` - Enable Cross-Origin isolation headers (true/false)
### CSP Implementation Details
@ -106,6 +109,20 @@ The Content Security Policy implementation in Hastebin uses nonces to secure inl
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
### Additional Security Headers
Besides CSP, Hastebin implements several other security headers:
1. **X-Content-Type-Options**: `nosniff` - Prevents MIME-type sniffing
2. **X-Frame-Options**: `DENY` - Prevents clickjacking attacks
3. **X-XSS-Protection**: `1; mode=block` - An additional layer of XSS protection
4. **Referrer-Policy**: `strict-origin-when-cross-origin` - Controls referrer information
5. **Permissions-Policy**: Restricts browser features (camera, microphone, geolocation, etc.)
6. **Cross-Origin-Embedder-Policy**: `require-corp` - Enhances cross-origin isolation
7. **Cross-Origin-Resource-Policy**: `same-origin` - Protects resources from unauthorized requests
8. **Cross-Origin-Opener-Policy**: `same-origin` - Helps with cross-origin isolation
9. **Strict-Transport-Security**: `max-age=31536000; includeSubDomains; preload` - Ensures HTTPS usage (when enabled)
#### Running in Development Mode
To run Hastebin with a more permissive CSP for development:

View File

@ -35,7 +35,12 @@ const config = {
// Allow unsafe-hashes in production for event handlers (default: true)
// This adds 'unsafe-hashes' to the policy for DOM event handlers
allowUnsafeHashes: process.env.HASTEBIN_ALLOW_UNSAFE_HASHES ?
(process.env.HASTEBIN_ALLOW_UNSAFE_HASHES.toLowerCase() === 'true') : true
(process.env.HASTEBIN_ALLOW_UNSAFE_HASHES.toLowerCase() === 'true') : true,
// Enable Cross-Origin isolation headers (default: false)
// This adds COOP, COEP, and CORP headers - can break some integrations
enableCrossOriginIsolation: process.env.HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION ?
(process.env.HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION.toLowerCase() === 'true') : false
},
// Logging configuration

View File

@ -1,22 +1,61 @@
services:
redis:
image: eqalpha/keydb
image: git.nixc.us/colin/haste:production-redis
volumes:
- redis_data:/data
networks:
- default
deploy:
placement:
constraints:
- node.hostname == macmini3
replicas: 1
restart_policy:
condition: on-failure
haste:
image: git.nixc.us/colin/haste:production-haste
volumes:
- public_system:/haste/public/system
environment:
- NODE_ENV=production
- HASTEBIN_ENABLE_CSP=true
- HASTEBIN_ENABLE_HSTS=true
- HASTEBIN_ALLOW_UNSAFE_HASHES=true
- HASTEBIN_SCRIPT_SOURCES=
- HASTEBIN_BYPASS_CSP_IN_DEV=false
networks:
- traefik
- default
deploy:
placement:
constraints:
- node.hostname == macmini3
labels:
homepage.group: apps
homepage.name: HasteBin
homepage.href: https://haste.nixc.us/
homepage.description: HasteBin
us.nixc.autodeploy: "true"
traefik.enable: "true"
traefik.http.routers.production-haste_haste.rule: "Host(`haste.nixc.us`)"
traefik.http.routers.production-haste_haste.entrypoints: "websecure"
traefik.http.routers.production-haste_haste.tls: "true"
traefik.http.routers.production-haste_haste.tls.certresolver: "letsencryptresolver"
traefik.http.routers.production-haste_haste.service: "production-haste_haste"
traefik.http.services.production-haste_haste.loadbalancer.server.port: "7777"
traefik.docker.network: "traefik"
replicas: 1
restart_policy:
condition: on-failure
networks:
traefik:
external: true
default:
driver: overlay
volumes:
public_system:
driver: local

View File

@ -6,104 +6,138 @@
const crypto = require('crypto');
const winston = require('winston');
// CSP middleware
function cspMiddleware(config) {
// Security headers middleware (renamed from CSP middleware as it now handles more headers)
function securityMiddleware(config) {
// Default to enabled if not specified
const enabled = config.security && typeof config.security.csp !== 'undefined' ?
const cspEnabled = config.security && typeof config.security.csp !== 'undefined' ?
config.security.csp : true;
// If CSP is disabled, return a no-op middleware
if (!enabled) {
// If security is entirely disabled, return a no-op middleware
// Note: This is different from just disabling CSP
if (config.security === false) {
return function(req, res, next) { next(); };
}
return function(req, res, next) {
// Only add CSP headers for HTML requests
// Only add security 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');
// Apply CSP headers if enabled
if (cspEnabled) {
// 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;
// Store nonce in request object for use in HTML template
req.cspNonce = nonce;
// Build the base script sources list
const scriptSources = ["'self'", `'nonce-${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 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)');
}
});
}
// 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
// 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'"
"font-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'none'",
"object-src 'none'"
];
winston.debug('Using development CSP policy with nonces (bypass disabled)');
winston.debug(`Using strict production CSP policy with nonces${config.security.allowUnsafeHashes ? " and unsafe-hashes" : ""}`);
}
} 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('; '));
} else {
winston.debug('CSP is disabled by configuration');
}
// Set the CSP header with the properly formatted policy
res.setHeader('Content-Security-Policy', cspDirectives.join('; '));
// Add other security headers - always applied regardless of CSP setting
// Add other security headers
// 1. X-Content-Type-Options - prevents MIME-type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// 2. X-Frame-Options - prevents clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// 3. X-XSS-Protection - legacy header, still used by some browsers
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'no-referrer');
// 4. Referrer-Policy - controls how much referrer information is included
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// 5. Permissions-Policy - controls browser features
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');
// Add Cross-Origin headers only if enabled in config
// These can be problematic for some applications, so we make them optional
const enableCrossOriginIsolation = config.security && config.security.enableCrossOriginIsolation;
if (enableCrossOriginIsolation) {
// 6. Cross-Origin-Embedder-Policy - for cross-origin isolation
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
// 7. Cross-Origin-Resource-Policy - protects resources from unauthorized requests
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
// 8. Cross-Origin-Opener-Policy - helps with cross-origin isolation
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
winston.debug('Added Cross-Origin isolation headers');
}
// If configured, add HSTS header
if (config.security && config.security.hsts) {
@ -116,4 +150,4 @@ function cspMiddleware(config) {
}
// Export the middleware
module.exports = cspMiddleware;
module.exports = securityMiddleware;

View File

@ -11,7 +11,7 @@ var connect_st = require('st');
var connect_rate_limit = require('connect-ratelimit');
var DocumentHandler = require('./lib/document_handler');
var cspMiddleware = require('./lib/csp');
var securityMiddleware = require('./lib/csp');
var templateHandler = require('./lib/template_handler');
// Load the configuration
@ -116,7 +116,7 @@ var documentHandler = new DocumentHandler({
var app = connect();
// Add CSP middleware early in the chain
app.use(cspMiddleware(config));
app.use(securityMiddleware(config));
// Add CORS support
app.use(function(req, res, next) {

View File

@ -56,7 +56,13 @@ const TESTS = {
name: 'Disabled CSP',
env: { NODE_ENV: 'production', HASTEBIN_ENABLE_CSP: 'false' },
expectedHeaders: {
'content-security-policy': false
'content-security-policy': false,
// Even with CSP disabled, these headers should still be present
'x-content-type-options': 'nosniff',
'x-frame-options': 'DENY',
'x-xss-protection': '1; mode=block',
'referrer-policy': 'strict-origin-when-cross-origin',
'permissions-policy': true
}
},
cors: {
@ -89,6 +95,24 @@ const TESTS = {
expectedHeaders: {
'content-security-policy': value => value.includes("'unsafe-inline'")
}
},
combinedSecurity: {
name: 'Combined Security Settings',
env: {
NODE_ENV: 'production',
HASTEBIN_ENABLE_CSP: 'false',
HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION: 'true',
HASTEBIN_ENABLE_HSTS: 'true'
},
expectedHeaders: {
'content-security-policy': false,
'x-content-type-options': 'nosniff',
'x-frame-options': 'DENY',
'cross-origin-embedder-policy': 'require-corp',
'cross-origin-resource-policy': 'same-origin',
'cross-origin-opener-policy': 'same-origin',
'strict-transport-security': true
}
}
};

View File

@ -229,7 +229,7 @@ run_tests() {
fi
if [[ -z "$test_filter" || "$test_filter" == *"noCsp"* ]]; then
if run_test "Disabled CSP" "NODE_ENV=production HASTEBIN_ENABLE_CSP=false" "content-security-policy:ABSENT"; then
if run_test "Disabled CSP" "NODE_ENV=production HASTEBIN_ENABLE_CSP=false" "content-security-policy:ABSENT,x-content-type-options:nosniff,x-frame-options:DENY"; then
passed=$((passed+1))
else
failed=$((failed+1))
@ -268,6 +268,14 @@ run_tests() {
fi
fi
if [[ -z "$test_filter" || "$test_filter" == *"combined"* ]]; then
if run_test "Combined Security Settings" "NODE_ENV=production HASTEBIN_ENABLE_CSP=false HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION=true HASTEBIN_ENABLE_HSTS=true" "content-security-policy:ABSENT,x-content-type-options:nosniff,x-frame-options:DENY,cross-origin-embedder-policy:require-corp,strict-transport-security:max-age"; then
passed=$((passed+1))
else
failed=$((failed+1))
fi
fi
# Cleanup any remaining server process
kill_server
@ -285,7 +293,7 @@ run_tests() {
# Show help if requested
if [ $# -gt 0 ] && [ "$1" == "--help" ]; then
echo "Usage: $0 [--test=test1,test2,...]"
echo "Available tests: basic, csp, noCsp, cors, hsts, devMode, devBypass"
echo "Available tests: basic, csp, noCsp, cors, hsts, devMode, devBypass, combined"
exit 0
fi