From c0502bc1a45f65a4a5616df69af17c5c21e9b7ac Mon Sep 17 00:00:00 2001 From: Leopere Date: Sat, 1 Mar 2025 18:24:46 -0500 Subject: [PATCH] Add security header testing scripts (both Node.js and Bash versions) --- package.json | 6 +- test-security.js | 251 +++++++++++++++++++++++++++++++++++++++ test-security.sh | 297 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 553 insertions(+), 1 deletion(-) create mode 100755 test-security.js create mode 100755 test-security.sh diff --git a/package.json b/package.json index 9343f4d..9f93da2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test-security.js b/test-security.js new file mode 100755 index 0000000..641e355 --- /dev/null +++ b/test-security.js @@ -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); +}); \ No newline at end of file diff --git a/test-security.sh b/test-security.sh new file mode 100755 index 0000000..1c2caf6 --- /dev/null +++ b/test-security.sh @@ -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 \ No newline at end of file