#!/usr/bin/env tsx /** * 安全响应头检查脚本 * * 检查部署网站的安全头配置是否合规,包括: * - 核心安全头(Content-Security-Policy, X-Content-Type-Options 等) * - CORS 配置 * - Cookie 安全标记 */ interface SecurityCheck { header: string; expected: string; actual: string | null; status: 'pass' | 'fail' | 'warn'; description: string; } interface CookieCheck { name: string; hasHttpOnly: boolean; hasSecure: boolean; hasSameSite: boolean; status: 'pass' | 'fail' | 'warn'; } function parseArgs(): string { const args = process.argv.slice(2); const urlIndex = args.indexOf('--url'); if (urlIndex !== -1 && args[urlIndex + 1]) { return args[urlIndex + 1]!; } return 'https://novalon.cn'; } function parseCookies(setCookieHeader: string | null): Array<{ name: string; flags: string[] }> { if (!setCookieHeader) return []; const cookies: Array<{ name: string; flags: string[] }> = []; // Handle multiple Set-Cookie headers (joined by comma-newline) const cookieStrings = setCookieHeader.split(/\n|,(?=\s*\w+=)/); for (const cookieStr of cookieStrings) { const trimmed = cookieStr.trim(); if (!trimmed) continue; const parts = trimmed.split(';').map((p) => p.trim()); const nameValue = parts[0]!; const name = nameValue.split('=')[0] || 'unknown'; const flags = parts.slice(1).map((f) => f.toLowerCase()); cookies.push({ name, flags }); } return cookies; } function checkCookies(cookies: Array<{ name: string; flags: string[] }>): CookieCheck[] { return cookies.map((cookie) => ({ name: cookie.name, hasHttpOnly: cookie.flags.some((f) => f === 'httponly'), hasSecure: cookie.flags.some((f) => f === 'secure'), hasSameSite: cookie.flags.some((f) => f.startsWith('samesite')), status: 'pass' as const, })); } async function checkSecurityHeaders(url: string): Promise<{ headerChecks: SecurityCheck[]; cookieChecks: CookieCheck[]; }> { console.log(`\n🔒 安全响应头检查 — ${url}\n`); const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(15000), }); const headers = response.headers; const headerChecks: SecurityCheck[] = [ { header: 'Content-Security-Policy', expected: '存在(需包含合理策略)', actual: headers.get('content-security-policy'), status: headers.get('content-security-policy') ? 'pass' : 'warn', description: '防止 XSS 和数据注入攻击的核心策略', }, { header: 'X-Content-Type-Options', expected: 'nosniff', actual: headers.get('x-content-type-options'), status: headers.get('x-content-type-options') === 'nosniff' ? 'pass' : 'fail', description: '防止 MIME 类型嗅探攻击', }, { header: 'X-Frame-Options', expected: 'DENY 或 SAMEORIGIN', actual: headers.get('x-frame-options'), status: (() => { const val = headers.get('x-frame-options'); if (val === 'DENY' || val === 'SAMEORIGIN') return 'pass' as const; return val ? 'warn' as const : 'fail' as const; })(), description: '防止点击劫持(Clickjacking)攻击', }, { header: 'Strict-Transport-Security', expected: '存在(需包含 max-age)', actual: headers.get('strict-transport-security'), status: headers.get('strict-transport-security') ? 'pass' : 'fail', description: '强制 HTTPS 连接,防止 SSL Strip 攻击', }, { header: 'Referrer-Policy', expected: '存在(如 strict-origin-when-cross-origin)', actual: headers.get('referrer-policy'), status: headers.get('referrer-policy') ? 'pass' : 'fail', description: '控制 Referer 头信息的发送策略', }, { header: 'Permissions-Policy', expected: '存在(需配置合理权限)', actual: headers.get('permissions-policy'), status: headers.get('permissions-policy') ? 'pass' : 'warn', description: '限制浏览器 API 权限(摄像头、麦克风等)', }, { header: 'X-XSS-Protection', expected: '存在(0 或 1; mode=block)', actual: headers.get('x-xss-protection'), status: (() => { const val = headers.get('x-xss-protection'); if (val === '0' || val === '1; mode=block') return 'pass' as const; return val ? 'warn' as const : 'warn' as const; })(), description: '已废弃的 XSS 过滤器(现代浏览器不再需要)', }, ]; // CORS check const corsOrigin = headers.get('access-control-allow-origin'); if (corsOrigin) { headerChecks.push({ header: 'Access-Control-Allow-Origin', expected: '非通配符(或仅对特定来源开放)', actual: corsOrigin, status: corsOrigin === '*' ? 'warn' : 'pass', description: 'CORS 跨域配置', }); } else { headerChecks.push({ header: 'Access-Control-Allow-Origin', expected: '无(不暴露 CORS 头)', actual: null, status: 'pass', description: 'CORS 跨域配置(未设置,符合安全预期)', }); } // Cookie security check const setCookie = headers.get('set-cookie'); const parsedCookies = parseCookies(setCookie); const cookieChecks = checkCookies(parsedCookies); return { headerChecks, cookieChecks }; } function printTable(checks: SecurityCheck[]): void { // Column widths const headerWidth = 34; const statusWidth = 6; const expectedWidth = 38; const actualWidth = 38; const separator = `├${'─'.repeat(headerWidth + 2)}┼${'─'.repeat(statusWidth + 2)}┼${'─'.repeat(expectedWidth + 2)}┼${'─'.repeat(actualWidth + 2)}┤`; const topBorder = `┌${'─'.repeat(headerWidth + 2)}┬${'─'.repeat(statusWidth + 2)}┬${'─'.repeat(expectedWidth + 2)}┬${'─'.repeat(actualWidth + 2)}┐`; const bottomBorder = `└${'─'.repeat(headerWidth + 2)}┴${'─'.repeat(statusWidth + 2)}┴${'─'.repeat(expectedWidth + 2)}┴${'─'.repeat(actualWidth + 2)}┘`; const headerRow = `│ ${'Header'.padEnd(headerWidth)} │ ${'Status'.padEnd(statusWidth)} │ ${'Expected'.padEnd(expectedWidth)} │ ${'Actual'.padEnd(actualWidth)} │`; console.log(topBorder); console.log(headerRow); console.log(separator); for (const check of checks) { const statusIcon = check.status === 'pass' ? '✅ PASS' : check.status === 'fail' ? '❌ FAIL' : '⚠️ WARN'; const actual = check.actual ?? '(未设置)'; const row = `│ ${check.header.padEnd(headerWidth)} │ ${statusIcon.padEnd(statusWidth + 2)} │ ${check.expected.padEnd(expectedWidth)} │ ${actual.padEnd(actualWidth)} │`; console.log(row); } console.log(bottomBorder); } function printCookieTable(cookieChecks: CookieCheck[]): void { if (cookieChecks.length === 0) { console.log('\n🍪 Cookie 安全标记: 无 Cookie 设置\n'); return; } const nameWidth = 24; const httpOnlyWidth = 10; const secureWidth = 8; const sameSiteWidth = 10; const topBorder = `┌${'─'.repeat(nameWidth + 2)}┬${'─'.repeat(httpOnlyWidth + 2)}┬${'─'.repeat(secureWidth + 2)}┬${'─'.repeat(sameSiteWidth + 2)}┐`; const separator = `├${'─'.repeat(nameWidth + 2)}┼${'─'.repeat(httpOnlyWidth + 2)}┼${'─'.repeat(secureWidth + 2)}┼${'─'.repeat(sameSiteWidth + 2)}┤`; const bottomBorder = `└${'─'.repeat(nameWidth + 2)}┴${'─'.repeat(httpOnlyWidth + 2)}┴${'─'.repeat(secureWidth + 2)}┴${'─'.repeat(sameSiteWidth + 2)}┘`; const headerRow = `│ ${'Cookie Name'.padEnd(nameWidth)} │ ${'HttpOnly'.padEnd(httpOnlyWidth)} │ ${'Secure'.padEnd(secureWidth)} │ ${'SameSite'.padEnd(sameSiteWidth)} │`; console.log('\n🍪 Cookie 安全标记\n'); console.log(topBorder); console.log(headerRow); console.log(separator); for (const cookie of cookieChecks) { const httpOnly = cookie.hasHttpOnly ? '✅' : '❌'; const secure = cookie.hasSecure ? '✅' : '❌'; const sameSite = cookie.hasSameSite ? '✅' : '❌'; const row = `│ ${cookie.name.padEnd(nameWidth)} │ ${httpOnly.padEnd(httpOnlyWidth)} │ ${secure.padEnd(secureWidth)} │ ${sameSite.padEnd(sameSiteWidth)} │`; console.log(row); } console.log(bottomBorder); } function printSummary(headerChecks: SecurityCheck[], cookieChecks: CookieCheck[]): void { const total = headerChecks.length; const passed = headerChecks.filter((c) => c.status === 'pass').length; const failed = headerChecks.filter((c) => c.status === 'fail').length; const warned = headerChecks.filter((c) => c.status === 'warn').length; const cookiePassed = cookieChecks.filter((c) => c.status === 'pass').length; const cookieFailed = cookieChecks.filter((c) => c.status === 'fail').length; console.log('\n📊 检查摘要\n'); console.log(` 安全头检查:`); console.log(` 总计: ${total}`); console.log(` ✅ 通过: ${passed}`); console.log(` ⚠️ 警告: ${warned}`); console.log(` ❌ 失败: ${failed}`); if (cookieChecks.length > 0) { console.log(`\n Cookie 安全标记:`); console.log(` 总计: ${cookieChecks.length}`); console.log(` ✅ 通过: ${cookiePassed}`); console.log(` ❌ 失败: ${cookieFailed}`); } } async function main() { const url = parseArgs(); try { const { headerChecks, cookieChecks } = await checkSecurityHeaders(url); // Print header check table console.log('安全头检查结果:'); printTable(headerChecks); // Print cookie check table printCookieTable(cookieChecks); // Print descriptions for failed/warned checks const issues = headerChecks.filter((c) => c.status !== 'pass'); if (issues.length > 0) { console.log('\n📝 说明:'); for (const issue of issues) { console.log(` • [${issue.status === 'fail' ? '❌' : '⚠️'}] ${issue.header}: ${issue.description}`); if (issue.status === 'fail') { console.log(` 期望: ${issue.expected}`); console.log(` 实际: ${issue.actual ?? '(未设置)'}`); } } } // Print summary printSummary(headerChecks, cookieChecks); // Determine exit code const hasFailures = headerChecks.some((c) => c.status === 'fail'); if (hasFailures) { console.log('\n❌ 存在失败的安全头检查项!'); process.exit(1); } else { console.log('\n✅ 所有关键安全头检查通过!'); process.exit(0); } } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(`\n❌ 检查执行失败: ${message}`); process.exit(1); } } main();