Compare commits
4 Commits
a88c7c6ccf
...
c5c44986d8
| Author | SHA1 | Date |
|---|---|---|
|
|
c5c44986d8 | |
|
|
758b242f11 | |
|
|
68afb6fc6c | |
|
|
c0502bc1a4 |
|
|
@ -0,0 +1,38 @@
|
||||||
|
FROM node:18-alpine as builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files first for better caching
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Clean npm cache and install dependencies with specific flags to avoid errors
|
||||||
|
RUN npm cache clean --force && \
|
||||||
|
npm install --production --no-optional && \
|
||||||
|
npm install mocha
|
||||||
|
|
||||||
|
# Copy the rest of the application
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Make sure app.sh is executable
|
||||||
|
RUN chmod +x app.sh
|
||||||
|
|
||||||
|
# Build assets
|
||||||
|
RUN node update-js.js
|
||||||
|
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy from builder stage
|
||||||
|
COPY --from=builder /app .
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
HASTEBIN_ENABLE_CSP=true \
|
||||||
|
HASTEBIN_ENABLE_HSTS=true
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 7777
|
||||||
|
|
||||||
|
# Use app.sh script as entry point
|
||||||
|
CMD ["/app/app.sh"]
|
||||||
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
|
"hsts": false, // Enable HTTP Strict Transport Security
|
||||||
"scriptSources": [], // Additional allowed script sources
|
"scriptSources": [], // Additional allowed script sources
|
||||||
"bypassCSPInDev": false, // Use permissive CSP in development mode
|
"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
|
* `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)
|
* `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)
|
||||||
|
* `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
|
### 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_SCRIPT_SOURCES` - Additional script sources (comma-separated)
|
||||||
* `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)
|
||||||
|
* `HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION` - Enable Cross-Origin isolation headers (true/false)
|
||||||
|
|
||||||
### CSP Implementation Details
|
### 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
|
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
|
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
|
#### Running in Development Mode
|
||||||
|
|
||||||
To run Hastebin with a more permissive CSP for development:
|
To run Hastebin with a more permissive CSP for development:
|
||||||
|
|
|
||||||
110
app.sh
110
app.sh
|
|
@ -23,49 +23,89 @@ case "${STORAGE_TYPE}" in
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Generate config.js from environment variables
|
# Generate config.js from environment variables
|
||||||
cat > config.js <<EOF
|
cat > config.js <<EOF
|
||||||
|
const config = {
|
||||||
|
// Server settings
|
||||||
|
host: "${HOST:-0.0.0.0}",
|
||||||
|
port: ${PORT:-7777},
|
||||||
|
keyLength: ${KEY_LENGTH:-10},
|
||||||
|
maxLength: ${MAX_LENGTH:-400000},
|
||||||
|
staticMaxAge: ${STATIC_MAX_AGE:-86400},
|
||||||
|
recompressStaticAssets: ${RECOMPRESS_STATIC_ASSETS:-true},
|
||||||
|
|
||||||
|
// Security settings
|
||||||
|
security: {
|
||||||
|
// Enable Content Security Policy
|
||||||
|
csp: ${HASTEBIN_ENABLE_CSP:-true},
|
||||||
|
|
||||||
|
// Enable HTTP Strict Transport Security (only enable in production with HTTPS)
|
||||||
|
hsts: ${HASTEBIN_ENABLE_HSTS:-false},
|
||||||
|
|
||||||
|
// Additional script sources (empty by default since we now host jQuery locally)
|
||||||
|
scriptSources: "${HASTEBIN_SCRIPT_SOURCES:-}".split(',').filter(Boolean),
|
||||||
|
|
||||||
|
// Allow bypassing strict CSP in development mode for testing (default: false)
|
||||||
|
bypassCSPInDev: ${HASTEBIN_BYPASS_CSP_IN_DEV:-false},
|
||||||
|
|
||||||
|
// Allow unsafe-hashes in production for event handlers (default: true)
|
||||||
|
allowUnsafeHashes: ${HASTEBIN_ALLOW_UNSAFE_HASHES:-true},
|
||||||
|
|
||||||
|
// Enable Cross-Origin isolation headers (default: false)
|
||||||
|
enableCrossOriginIsolation: ${HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION:-false}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Logging configuration
|
||||||
|
logging: [
|
||||||
{
|
{
|
||||||
"host": "${HOST:-0.0.0.0}",
|
level: "${LOGGING_LEVEL:-verbose}",
|
||||||
"port": ${PORT:-7777},
|
type: "${LOGGING_TYPE:-Console}",
|
||||||
"keyLength": ${KEY_LENGTH:-10},
|
colorize: ${LOGGING_COLORIZE:-false},
|
||||||
"maxLength": ${MAX_LENGTH:-400000},
|
json: ${LOGGING_JSON:-false}
|
||||||
"staticMaxAge": ${STATIC_MAX_AGE:-7776000},
|
|
||||||
"recompressStaticAssets": ${RECOMPRESS_STATIC_ASSETS:-true},
|
|
||||||
"logging": [
|
|
||||||
{
|
|
||||||
"level": "${LOGGING_LEVEL:-verbose}",
|
|
||||||
"type": "${LOGGING_TYPE:-Console}",
|
|
||||||
"colorize": ${LOGGING_COLORIZE:-false}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"keyGenerator": {
|
|
||||||
"type": "${KEY_GENERATOR_TYPE:-phonetic}"
|
|
||||||
},
|
|
||||||
"storage": {
|
|
||||||
"type": "${STORAGE_TYPE:-redis}",
|
|
||||||
"path": "${STORAGE_PATH:-./data}",
|
|
||||||
"host": "${STORAGE_HOST:-haste_redis}",
|
|
||||||
"port": ${STORAGE_PORT:-6379},
|
|
||||||
"db": ${STORAGE_DB:-2},
|
|
||||||
"expire": ${STORAGE_EXPIRE:-2592000}
|
|
||||||
|
|
||||||
|
|
||||||
|
// Key generator configuration
|
||||||
|
keyGenerator: {
|
||||||
|
type: "${KEY_GENERATOR_TYPE:-phonetic}"
|
||||||
},
|
},
|
||||||
|
|
||||||
"storage": {
|
// Rate limiting configuration
|
||||||
"type": "${STORAGE_TYPE:-redis}",
|
rateLimits: {
|
||||||
"path": "${STORAGE_PATH:-./data}",
|
categories: {
|
||||||
"host": "${STORAGE_HOST:-haste_redis}",
|
normal: {
|
||||||
"port": ${STORAGE_PORT:-6379},
|
totalRequests: ${RATE_LIMIT_TOTAL_REQUESTS:-500},
|
||||||
"db": ${STORAGE_DB:-2},
|
every: ${RATE_LIMIT_WINDOW:-60000}
|
||||||
"expire": ${STORAGE_EXPIRE:-2592000}
|
|
||||||
},
|
|
||||||
|
|
||||||
"documents": {
|
|
||||||
"about": "./about.md"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Storage configuration
|
||||||
|
storage: {
|
||||||
|
type: "${STORAGE_TYPE:-redis}",
|
||||||
|
path: "${STORAGE_PATH:-./data}",
|
||||||
|
host: "${STORAGE_HOST:-redis}",
|
||||||
|
port: ${STORAGE_PORT:-6379},
|
||||||
|
password: "${STORAGE_PASSWORD:-}",
|
||||||
|
db: ${STORAGE_DB:-0},
|
||||||
|
expire: ${STORAGE_EXPIRE:-7776000}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Static documents
|
||||||
|
documents: {
|
||||||
|
about: "${ABOUT_DOCUMENT:-./about.md}"
|
||||||
|
},
|
||||||
|
|
||||||
|
// CORS settings
|
||||||
|
allowedOrigins: "${HASTEBIN_ALLOWED_ORIGINS:-*}".split(',')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Support for backwards compatibility
|
||||||
|
if (process.env.REDIS_URL || process.env.REDISTOGO_URL) {
|
||||||
|
config.storage.url = process.env.REDIS_URL || process.env.REDISTOGO_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# If TEST_MODE is true, install Mocha and run tests
|
# If TEST_MODE is true, install Mocha and run tests
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,12 @@ const config = {
|
||||||
// Allow unsafe-hashes in production for event handlers (default: true)
|
// Allow unsafe-hashes in production for event handlers (default: true)
|
||||||
// This adds 'unsafe-hashes' to the policy for DOM event handlers
|
// This adds 'unsafe-hashes' to the policy for DOM event handlers
|
||||||
allowUnsafeHashes: process.env.HASTEBIN_ALLOW_UNSAFE_HASHES ?
|
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
|
// Logging configuration
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,64 @@
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: eqalpha/keydb
|
image: redis:alpine
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
deploy:
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.hostname == macmini3
|
||||||
|
replicas: 1
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
|
||||||
haste:
|
haste:
|
||||||
image: git.nixc.us/colin/haste:production-haste
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
volumes:
|
volumes:
|
||||||
- public_system:/haste/public/system
|
- public_system:/app/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
|
||||||
|
- HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION=true
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
- default
|
- 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:
|
networks:
|
||||||
traefik:
|
traefik:
|
||||||
external: true
|
external: true
|
||||||
default:
|
default:
|
||||||
driver: overlay
|
driver: overlay
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
public_system:
|
public_system:
|
||||||
driver: local
|
driver: local
|
||||||
|
|
|
||||||
52
lib/csp.js
52
lib/csp.js
|
|
@ -6,22 +6,25 @@
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
|
|
||||||
// CSP middleware
|
// Security headers middleware (renamed from CSP middleware as it now handles more headers)
|
||||||
function cspMiddleware(config) {
|
function securityMiddleware(config) {
|
||||||
// Default to enabled if not specified
|
// 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;
|
config.security.csp : true;
|
||||||
|
|
||||||
// If CSP is disabled, return a no-op middleware
|
// If security is entirely disabled, return a no-op middleware
|
||||||
if (!enabled) {
|
// Note: This is different from just disabling CSP
|
||||||
|
if (config.security === false) {
|
||||||
return function(req, res, next) { next(); };
|
return function(req, res, next) { next(); };
|
||||||
}
|
}
|
||||||
|
|
||||||
return function(req, res, 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_-]+$/);
|
const isHtmlRequest = req.url === '/' || req.url.match(/^\/[a-zA-Z0-9_-]+$/);
|
||||||
|
|
||||||
if (isHtmlRequest) {
|
if (isHtmlRequest) {
|
||||||
|
// Apply CSP headers if enabled
|
||||||
|
if (cspEnabled) {
|
||||||
// Generate a unique nonce for this request
|
// Generate a unique nonce for this request
|
||||||
const nonce = crypto.randomBytes(16).toString('base64');
|
const nonce = crypto.randomBytes(16).toString('base64');
|
||||||
|
|
||||||
|
|
@ -98,12 +101,43 @@ function cspMiddleware(config) {
|
||||||
|
|
||||||
// Set the CSP header with the properly formatted policy
|
// Set the CSP header with the properly formatted policy
|
||||||
res.setHeader('Content-Security-Policy', cspDirectives.join('; '));
|
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');
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||||
|
|
||||||
|
// 2. X-Frame-Options - prevents clickjacking
|
||||||
res.setHeader('X-Frame-Options', 'DENY');
|
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('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 configured, add HSTS header
|
||||||
if (config.security && config.security.hsts) {
|
if (config.security && config.security.hsts) {
|
||||||
|
|
@ -116,4 +150,4 @@ function cspMiddleware(config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export the middleware
|
// Export the middleware
|
||||||
module.exports = cspMiddleware;
|
module.exports = securityMiddleware;
|
||||||
|
|
@ -53,7 +53,12 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"test": "mocha --recursive"
|
"test": "mocha --recursive",
|
||||||
|
"test:security": "node test-security.js",
|
||||||
|
"test:security:bash": "./test-security.sh",
|
||||||
|
"test:security:csp": "node test-security.js --test=csp",
|
||||||
|
"test:security:cors": "node test-security.js --test=cors",
|
||||||
|
"build": "node update-js.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ var connect_st = require('st');
|
||||||
var connect_rate_limit = require('connect-ratelimit');
|
var connect_rate_limit = require('connect-ratelimit');
|
||||||
|
|
||||||
var DocumentHandler = require('./lib/document_handler');
|
var DocumentHandler = require('./lib/document_handler');
|
||||||
var cspMiddleware = require('./lib/csp');
|
var securityMiddleware = require('./lib/csp');
|
||||||
var templateHandler = require('./lib/template_handler');
|
var templateHandler = require('./lib/template_handler');
|
||||||
|
|
||||||
// Load the configuration
|
// Load the configuration
|
||||||
|
|
@ -116,7 +116,7 @@ var documentHandler = new DocumentHandler({
|
||||||
var app = connect();
|
var app = connect();
|
||||||
|
|
||||||
// Add CSP middleware early in the chain
|
// Add CSP middleware early in the chain
|
||||||
app.use(cspMiddleware(config));
|
app.use(securityMiddleware(config));
|
||||||
|
|
||||||
// Add CORS support
|
// Add CORS support
|
||||||
app.use(function(req, res, next) {
|
app.use(function(req, res, next) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,275 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security Headers Testing Script for Hastebin
|
||||||
|
*
|
||||||
|
* This script tests various security header configurations by:
|
||||||
|
* 1. Starting the server with different security settings
|
||||||
|
* 2. Making HTTP requests to check the headers
|
||||||
|
* 3. Validating basic functionality works
|
||||||
|
* 4. Reporting results
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node test-security.js
|
||||||
|
*
|
||||||
|
* Or run specific tests:
|
||||||
|
* node test-security.js --test=csp,cors
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { exec, spawn } = require('child_process');
|
||||||
|
const http = require('http');
|
||||||
|
const assert = require('assert').strict;
|
||||||
|
const { promisify } = require('util');
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const PORT = 7777;
|
||||||
|
const HOST = 'localhost';
|
||||||
|
const SERVER_START_WAIT = 2000; // Time to wait for server to start (ms)
|
||||||
|
|
||||||
|
// Test cases
|
||||||
|
const TESTS = {
|
||||||
|
basic: {
|
||||||
|
name: 'Basic Security Headers',
|
||||||
|
env: { NODE_ENV: 'production' },
|
||||||
|
expectedHeaders: {
|
||||||
|
'content-security-policy': true,
|
||||||
|
'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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
csp: {
|
||||||
|
name: 'Content Security Policy',
|
||||||
|
env: { NODE_ENV: 'production', HASTEBIN_ENABLE_CSP: 'true' },
|
||||||
|
expectedHeaders: {
|
||||||
|
'content-security-policy': (value) => {
|
||||||
|
return value.includes("script-src 'self'") &&
|
||||||
|
value.includes("'nonce-") &&
|
||||||
|
(value.includes("'unsafe-hashes'") || true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
noCsp: {
|
||||||
|
name: 'Disabled CSP',
|
||||||
|
env: { NODE_ENV: 'production', HASTEBIN_ENABLE_CSP: 'false' },
|
||||||
|
expectedHeaders: {
|
||||||
|
'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: {
|
||||||
|
name: 'Cross-Origin Isolation',
|
||||||
|
env: { NODE_ENV: 'production', HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION: 'true' },
|
||||||
|
expectedHeaders: {
|
||||||
|
'cross-origin-embedder-policy': 'require-corp',
|
||||||
|
'cross-origin-resource-policy': 'same-origin',
|
||||||
|
'cross-origin-opener-policy': 'same-origin'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hsts: {
|
||||||
|
name: 'HTTP Strict Transport Security',
|
||||||
|
env: { NODE_ENV: 'production', HASTEBIN_ENABLE_HSTS: 'true' },
|
||||||
|
expectedHeaders: {
|
||||||
|
'strict-transport-security': true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
devMode: {
|
||||||
|
name: 'Development Mode',
|
||||||
|
env: { NODE_ENV: 'development' },
|
||||||
|
expectedHeaders: {
|
||||||
|
'content-security-policy': true,
|
||||||
|
'x-content-type-options': 'nosniff'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
devBypass: {
|
||||||
|
name: 'Development Mode with CSP Bypass',
|
||||||
|
env: { NODE_ENV: 'development', HASTEBIN_BYPASS_CSP_IN_DEV: 'true' },
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to make HTTP requests and check headers
|
||||||
|
async function checkHeaders(testCase) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = http.request({
|
||||||
|
hostname: HOST,
|
||||||
|
port: PORT,
|
||||||
|
path: '/',
|
||||||
|
method: 'GET'
|
||||||
|
}, (res) => {
|
||||||
|
const headers = res.headers;
|
||||||
|
const failures = [];
|
||||||
|
|
||||||
|
// Check expected headers
|
||||||
|
for (const [header, expected] of Object.entries(testCase.expectedHeaders)) {
|
||||||
|
if (expected === false) {
|
||||||
|
// Header should not be present
|
||||||
|
if (header in headers) {
|
||||||
|
failures.push(`Expected header '${header}' to be absent, but found: ${headers[header]}`);
|
||||||
|
}
|
||||||
|
} else if (expected === true) {
|
||||||
|
// Header should be present (any value)
|
||||||
|
if (!(header in headers)) {
|
||||||
|
failures.push(`Expected header '${header}' to be present, but it was missing`);
|
||||||
|
}
|
||||||
|
} else if (typeof expected === 'function') {
|
||||||
|
// Custom validator function
|
||||||
|
if (!(header in headers) || !expected(headers[header])) {
|
||||||
|
failures.push(`Header '${header}' failed validation: ${headers[header]}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Exact value match
|
||||||
|
if (headers[header] !== expected) {
|
||||||
|
failures.push(`Header '${header}' expected '${expected}' but got '${headers[header]}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
reject(new Error(failures.join('\n')));
|
||||||
|
} else {
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (err) => {
|
||||||
|
reject(new Error(`Request failed: ${err.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test functionality by creating and retrieving a document
|
||||||
|
async function testFunctionality() {
|
||||||
|
// Create a document
|
||||||
|
const createResult = await execAsync(`curl -s -X POST http://${HOST}:${PORT}/documents -d "Security Test Document"`);
|
||||||
|
const { key } = JSON.parse(createResult.stdout);
|
||||||
|
|
||||||
|
if (!key || typeof key !== 'string') {
|
||||||
|
throw new Error('Failed to create document - invalid response');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the document
|
||||||
|
const getResult = await execAsync(`curl -s http://${HOST}:${PORT}/raw/${key}`);
|
||||||
|
|
||||||
|
if (getResult.stdout.trim() !== "Security Test Document") {
|
||||||
|
throw new Error(`Document retrieval failed - expected "Security Test Document" but got "${getResult.stdout.trim()}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run a single test
|
||||||
|
async function runTest(testName) {
|
||||||
|
if (!(testName in TESTS)) {
|
||||||
|
console.error(`Unknown test: ${testName}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const test = TESTS[testName];
|
||||||
|
console.log(`\n🔒 Running test: ${test.name} (${testName})`);
|
||||||
|
|
||||||
|
// Start server with test configuration
|
||||||
|
const env = { ...process.env, ...test.env };
|
||||||
|
const serverProcess = spawn('node', ['test-local.js'], {
|
||||||
|
env,
|
||||||
|
stdio: 'ignore',
|
||||||
|
detached: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for server to start
|
||||||
|
await new Promise(resolve => setTimeout(resolve, SERVER_START_WAIT));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check headers
|
||||||
|
await checkHeaders(test);
|
||||||
|
console.log(`✅ Headers check passed for ${test.name}`);
|
||||||
|
|
||||||
|
// Check functionality
|
||||||
|
await testFunctionality();
|
||||||
|
console.log(`✅ Functionality check passed for ${test.name}`);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Test failed: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
// Kill server process and its children
|
||||||
|
process.kill(-serverProcess.pid);
|
||||||
|
serverProcess.unref();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all tests or specified tests
|
||||||
|
async function runTests() {
|
||||||
|
console.log('🔒 Hastebin Security Headers Test Suite 🔒');
|
||||||
|
|
||||||
|
// Check if specific tests were requested
|
||||||
|
const testArg = process.argv.find(arg => arg.startsWith('--test='));
|
||||||
|
let testsToRun = Object.keys(TESTS);
|
||||||
|
|
||||||
|
if (testArg) {
|
||||||
|
testsToRun = testArg.replace('--test=', '').split(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const testName of testsToRun) {
|
||||||
|
try {
|
||||||
|
const success = await runTest(testName);
|
||||||
|
if (success) {
|
||||||
|
passed++;
|
||||||
|
} else {
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Test execution error: ${error.message}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay between tests
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n📊 Test Results:');
|
||||||
|
console.log(`✅ ${passed} tests passed`);
|
||||||
|
console.log(`❌ ${failed} tests failed`);
|
||||||
|
|
||||||
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run tests
|
||||||
|
runTests().catch(err => {
|
||||||
|
console.error('Test suite error:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,305 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Security Headers Testing Script for Hastebin
|
||||||
|
# This script tests various security header configurations by running curl commands
|
||||||
|
# to verify headers are correctly set and the application works properly.
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
PORT=7777
|
||||||
|
HOST=localhost
|
||||||
|
SERVER_START_WAIT=5 # seconds
|
||||||
|
KILL_WAIT=2 # seconds
|
||||||
|
|
||||||
|
# Utility functions
|
||||||
|
print_header() {
|
||||||
|
echo -e "\n${BLUE}$1${NC}"
|
||||||
|
echo -e "${BLUE}$(printf '=%.0s' $(seq 1 ${#1}))${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}✅ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}❌ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info() {
|
||||||
|
echo -e "${YELLOW}ℹ️ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Kill any running server instance
|
||||||
|
kill_server() {
|
||||||
|
pkill -f "node test-local.js" >/dev/null 2>&1
|
||||||
|
sleep $KILL_WAIT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start server with specified environment variables
|
||||||
|
start_server() {
|
||||||
|
print_info "Starting server with: $1"
|
||||||
|
|
||||||
|
# Use nohup to ensure the process continues running even if the script is interrupted
|
||||||
|
eval "HASTEBIN_STORAGE_TYPE=file $1 node test-local.js > /tmp/hastebin-test.log 2>&1 &"
|
||||||
|
|
||||||
|
# Store the PID for later cleanup
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Wait for server to start and log the process ID
|
||||||
|
print_info "Started server process with PID: $SERVER_PID, waiting ${SERVER_START_WAIT}s..."
|
||||||
|
sleep $SERVER_START_WAIT
|
||||||
|
|
||||||
|
# Check if the server is actually running
|
||||||
|
if ! ps -p $SERVER_PID > /dev/null; then
|
||||||
|
print_error "Server failed to start! Check logs at /tmp/hastebin-test.log"
|
||||||
|
cat /tmp/hastebin-test.log | head -n 20
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if a header exists and matches expected value
|
||||||
|
check_header() {
|
||||||
|
local header="$1"
|
||||||
|
local expected="$2"
|
||||||
|
local response="$3"
|
||||||
|
|
||||||
|
# Extract the header value from response
|
||||||
|
local value=$(echo "$response" | grep -i "^$header:" | sed "s/^$header: //i" | tr -d '\r')
|
||||||
|
|
||||||
|
if [ -z "$value" ]; then
|
||||||
|
if [ "$expected" == "ABSENT" ]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Header '$header' is missing"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ "$expected" == "ABSENT" ]; then
|
||||||
|
print_error "Header '$header' should be absent but found: $value"
|
||||||
|
return 1
|
||||||
|
elif [ "$expected" == "ANY" ]; then
|
||||||
|
return 0
|
||||||
|
elif [[ "$value" == *"$expected"* ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Header '$header' expected to contain '$expected' but got '$value'"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test functionality by creating and retrieving a document
|
||||||
|
test_functionality() {
|
||||||
|
print_info "Testing document creation and retrieval..."
|
||||||
|
|
||||||
|
# Try multiple times with backoff
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
# Create a document
|
||||||
|
local create_response=$(curl -s -X POST http://$HOST:$PORT/documents -d "Security Test Document")
|
||||||
|
echo "Create response: $create_response"
|
||||||
|
|
||||||
|
# Extract the key using a more reliable method
|
||||||
|
local key=$(echo $create_response | sed -n 's/.*"key":"\([^"]*\)".*/\1/p')
|
||||||
|
|
||||||
|
if [ -n "$key" ]; then
|
||||||
|
print_info "Created document with key: $key"
|
||||||
|
|
||||||
|
# Retrieve the document
|
||||||
|
local get_response=$(curl -s http://$HOST:$PORT/raw/$key)
|
||||||
|
|
||||||
|
if [ "$get_response" == "Security Test Document" ]; then
|
||||||
|
print_success "Document creation and retrieval successful"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Document retrieval failed - expected 'Security Test Document' but got '$get_response'"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Failed to extract key from response: $create_response"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If we reach here, something failed - wait and retry
|
||||||
|
sleep_time=$((attempt * 2))
|
||||||
|
print_info "Attempt $attempt failed, waiting ${sleep_time}s before retry..."
|
||||||
|
sleep $sleep_time
|
||||||
|
done
|
||||||
|
|
||||||
|
print_error "Failed to create or retrieve document after 3 attempts"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run a single test
|
||||||
|
run_test() {
|
||||||
|
local test_name="$1"
|
||||||
|
local env_vars="$2"
|
||||||
|
local headers_to_check="$3"
|
||||||
|
|
||||||
|
print_header "Running test: $test_name"
|
||||||
|
|
||||||
|
# Kill any existing server and start a new one with the specified env
|
||||||
|
kill_server
|
||||||
|
if ! start_server "$env_vars"; then
|
||||||
|
print_error "Could not start server for test '$test_name'"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get headers - retry a few times if needed
|
||||||
|
local response=""
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
response=$(curl -I -s http://$HOST:$PORT/)
|
||||||
|
if [[ "$response" == *"HTTP/1."* ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$response" != *"HTTP/1."* ]]; then
|
||||||
|
print_error "Could not get response from server"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check each header
|
||||||
|
local failed=0
|
||||||
|
|
||||||
|
# Parse the headers to check and their expected values
|
||||||
|
local IFS=','
|
||||||
|
read -ra HEADER_CHECKS <<< "$headers_to_check"
|
||||||
|
for check in "${HEADER_CHECKS[@]}"; do
|
||||||
|
local header=$(echo $check | cut -d: -f1)
|
||||||
|
local expected=$(echo $check | cut -d: -f2)
|
||||||
|
|
||||||
|
if ! check_header "$header" "$expected" "$response"; then
|
||||||
|
failed=1
|
||||||
|
else
|
||||||
|
print_success "Header '$header' check passed"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test functionality
|
||||||
|
if ! test_functionality; then
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $failed -eq 0 ]; then
|
||||||
|
print_success "Test '$test_name' passed"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Test '$test_name' failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
run_tests() {
|
||||||
|
print_header "🔒 Hastebin Security Headers Test Suite 🔒"
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
local test_filter=""
|
||||||
|
if [[ "$1" == --test=* ]]; then
|
||||||
|
test_filter="${1#--test=}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local passed=0
|
||||||
|
local failed=0
|
||||||
|
|
||||||
|
# Run selected tests
|
||||||
|
|
||||||
|
# Filter by name if specified
|
||||||
|
if [[ -z "$test_filter" || "$test_filter" == *"basic"* ]]; then
|
||||||
|
if run_test "Basic Security Headers" "NODE_ENV=production" "content-security-policy:ANY,x-content-type-options:nosniff,x-frame-options:DENY,x-xss-protection:1; mode=block,referrer-policy:strict-origin-when-cross-origin,permissions-policy:ANY"; then
|
||||||
|
passed=$((passed+1))
|
||||||
|
else
|
||||||
|
failed=$((failed+1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$test_filter" || "$test_filter" == *"csp"* ]]; then
|
||||||
|
if run_test "Content Security Policy" "NODE_ENV=production HASTEBIN_ENABLE_CSP=true" "content-security-policy:script-src"; then
|
||||||
|
passed=$((passed+1))
|
||||||
|
else
|
||||||
|
failed=$((failed+1))
|
||||||
|
fi
|
||||||
|
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,x-content-type-options:nosniff,x-frame-options:DENY"; then
|
||||||
|
passed=$((passed+1))
|
||||||
|
else
|
||||||
|
failed=$((failed+1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$test_filter" || "$test_filter" == *"cors"* ]]; then
|
||||||
|
if run_test "Cross-Origin Isolation" "NODE_ENV=production HASTEBIN_ENABLE_CROSS_ORIGIN_ISOLATION=true" "cross-origin-embedder-policy:require-corp,cross-origin-resource-policy:same-origin,cross-origin-opener-policy:same-origin"; then
|
||||||
|
passed=$((passed+1))
|
||||||
|
else
|
||||||
|
failed=$((failed+1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$test_filter" || "$test_filter" == *"hsts"* ]]; then
|
||||||
|
if run_test "HTTP Strict Transport Security" "NODE_ENV=production HASTEBIN_ENABLE_HSTS=true" "strict-transport-security:max-age"; then
|
||||||
|
passed=$((passed+1))
|
||||||
|
else
|
||||||
|
failed=$((failed+1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$test_filter" || "$test_filter" == *"devMode"* ]]; then
|
||||||
|
if run_test "Development Mode" "NODE_ENV=development" "content-security-policy:ANY,x-content-type-options:nosniff"; then
|
||||||
|
passed=$((passed+1))
|
||||||
|
else
|
||||||
|
failed=$((failed+1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$test_filter" || "$test_filter" == *"devBypass"* ]]; then
|
||||||
|
if run_test "Development Mode with CSP Bypass" "NODE_ENV=development HASTEBIN_BYPASS_CSP_IN_DEV=true" "content-security-policy:unsafe-inline"; then
|
||||||
|
passed=$((passed+1))
|
||||||
|
else
|
||||||
|
failed=$((failed+1))
|
||||||
|
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
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
print_header "📊 Test Results:"
|
||||||
|
print_success "$passed tests passed"
|
||||||
|
if [ $failed -gt 0 ]; then
|
||||||
|
print_error "$failed tests failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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, combined"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run tests with any arguments passed
|
||||||
|
run_tests "$@"
|
||||||
|
result=$?
|
||||||
|
|
||||||
|
# Exit with status
|
||||||
|
exit $result
|
||||||
Loading…
Reference in New Issue