357 lines
11 KiB
JavaScript
Executable File
357 lines
11 KiB
JavaScript
Executable File
#!/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();
|