Add comprehensive security headers support with testing framework
This commit is contained in:
parent
c0502bc1a4
commit
68afb6fc6c
19
README.md
19
README.md
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
52
lib/csp.js
52
lib/csp.js
|
@ -6,22 +6,25 @@
|
|||
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) {
|
||||
// Apply CSP headers if enabled
|
||||
if (cspEnabled) {
|
||||
// Generate a unique nonce for this request
|
||||
const nonce = crypto.randomBytes(16).toString('base64');
|
||||
|
||||
|
@ -98,12 +101,43 @@ function cspMiddleware(config) {
|
|||
|
||||
// Set the CSP header with the properly formatted policy
|
||||
res.setHeader('Content-Security-Policy', cspDirectives.join('; '));
|
||||
} else {
|
||||
winston.debug('CSP is disabled by configuration');
|
||||
}
|
||||
|
||||
// Add other security headers
|
||||
// Add other security headers - always applied regardless of CSP setting
|
||||
|
||||
// 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;
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue