#!/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); });