#!/usr/bin/env node /** * Security Headers Testing Script for Hastebin * Tests various security header configurations * * Usage: * node test/security/security_spec.js * node test/security/security_spec.js --test=csp,cors */ const http = require('http'); const { exec } = require('child_process'); const util = require('util'); const path = require('path'); const fs = require('fs'); // Convert exec to Promise-based const execAsync = util.promisify(exec); // Test configuration const HOST = 'localhost'; const PORT = 7777; const SERVER_START_WAIT = 1000; // ms to wait for server to start const SERVER_STOP_WAIT = 1000; // ms to wait for server to stop // Use absolute paths to avoid path resolution issues const rootDir = path.resolve(__dirname, '../..'); // 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 = []; console.log('Received headers:'); Object.entries(headers).forEach(([key, value]) => { console.log(`- ${key}: ${value}`); }); // 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() { try { // 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; } catch (error) { console.error('Error in functionality test:', error); throw error; } } // Helper to kill any existing processes on the test port async function killExistingProcesses() { try { // Find processes using the port const { stdout } = await execAsync(`lsof -i :${PORT} -t || echo ""`); if (stdout.trim()) { const pids = stdout.trim().split('\n'); console.log(`Terminating existing processes on port ${PORT}: ${pids.join(', ')}`); // Kill each process gracefully first for (const pid of pids) { if (pid.trim()) { try { // Try SIGTERM first (graceful) await execAsync(`kill ${pid}`); // Wait a bit for process to terminate await new Promise(resolve => setTimeout(resolve, 500)); // Check if process is still running try { await execAsync(`ps -p ${pid} || echo ""`); // If we get here, process is still running, use SIGKILL await execAsync(`kill -9 ${pid}`); } catch (e) { // Process already terminated, which is good } } catch (err) { console.error(`Error terminating process ${pid}:`, err.message); } } } // Wait for processes to terminate await new Promise(resolve => setTimeout(resolve, SERVER_STOP_WAIT)); } } catch (error) { // Just log the error and continue console.error('Error checking for existing processes:', error.message); } } // 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})`); // Kill any existing processes on the test port await killExistingProcesses(); // Set environment variables for this test Object.entries(test.env).forEach(([key, value]) => { process.env[key] = value; }); // Use our test-local.js module const testLocalPath = path.join(rootDir, 'test/utils/test-local.js'); let testServer; try { // Clear the require cache to ensure a fresh server for each test Object.keys(require.cache).forEach(key => { delete require.cache[key]; }); // Start server with test configuration testServer = require(testLocalPath); // Wait for server to start await new Promise(resolve => setTimeout(resolve, SERVER_START_WAIT)); // 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 { // Clean up server if (testServer && testServer.cleanup) { await testServer.cleanup(false); } // Make sure the server is really stopped await killExistingProcesses(); // Clear the require cache to ensure a fresh server for the next test Object.keys(require.cache).forEach(key => { delete require.cache[key]; }); // Reset environment variables Object.keys(test.env).forEach(key => { delete process.env[key]; }); } } // Run all tests or specified tests async function runTests() { console.log('šŸ”’ Hastebin Security Headers Test Suite šŸ”’'); // Kill any existing processes before starting await killExistingProcesses(); // 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; const results = []; for (const testName of testsToRun) { try { const success = await runTest(testName); if (success) { passed++; results.push(`āœ… ${TESTS[testName].name}`); } else { failed++; results.push(`āŒ ${TESTS[testName].name}`); } } catch (error) { console.error(`āŒ Test execution error: ${error.message}`); failed++; results.push(`āŒ ${TESTS[testName].name} (execution error)`); } } console.log(`\nšŸ“Š Test Results Summary:`); for (const result of results) { console.log(result); } console.log(`\nāœ… ${passed} tests passed`); console.log(`āŒ ${failed} tests failed`); // Exit with appropriate code process.exit(failed > 0 ? 1 : 0); } // Run the tests runTests();