Add security header testing scripts (both Node.js and Bash versions)

This commit is contained in:
Leopere 2025-03-01 18:24:46 -05:00
parent a88c7c6ccf
commit c0502bc1a4
3 changed files with 553 additions and 1 deletions

View File

@ -53,7 +53,11 @@
},
"scripts": {
"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"
},
"repository": {
"type": "git",

251
test-security.js Executable file
View File

@ -0,0 +1,251 @@
#!/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
}
},
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'")
}
}
};
// 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);
});

297
test-security.sh Executable file
View File

@ -0,0 +1,297 @@
#!/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"; 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
# 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"
exit 0
fi
# Run tests with any arguments passed
run_tests "$@"
result=$?
# Exit with status
exit $result