Web安全响应头最佳实践:从CSP到HSTS的5种生产模式
你的网站可能正在"裸奔":安全响应头缺失的真实代价
2026年4月,某电商平台因缺少Content-Security-Policy头,被攻击者通过第三方CDN注入恶意脚本,导致12万用户支付信息泄露。事后排查发现,只需一行响应头配置就能阻止这次攻击:
Content-Security-Policy: script-src 'self' 'nonce-abc123'
安全响应头是Web安全的"第一道门锁"——不需要改一行业务代码,只需在服务器配置中添加几行HTTP头,就能防御XSS、点击劫持、协议降级等多种攻击。
现实数据:根据Mozilla Observatory 2026年的扫描报告,全球Top 10000网站中,仅有31%正确配置了CSP,18%启用了HSTS preload,超过40%的网站缺少至少一项关键安全响应头。
核心概念速查表
| 响应头 | 全称 | 防御目标 | 浏览器支持 | 优先级 |
|---|---|---|---|---|
| CSP | Content-Security-Policy | XSS、数据注入、代码注入 | 全平台 | 🔴 Critical |
| HSTS | Strict-Transport-Security | 协议降级、SSL剥离 | 全平台 | 🔴 Critical |
| CORS | Cross-Origin Resource Sharing | 跨域资源访问控制 | 全平台 | 🔴 Critical |
| Permissions-Policy | Permissions-Policy | 浏览器功能权限控制 | Chrome/Edge/Safari | 🟡 High |
| X-Content-Type-Options | X-Content-Type-Options | MIME嗅探攻击 | 全平台 | 🟡 High |
| X-Frame-Options | X-Frame-Options | 点击劫持 | 全平台(已被CSP替代) | 🟢 Medium |
| Referrer-Policy | Referrer-Policy | Referer信息泄露 | 全平台 | 🟡 High |
问题剖析:安全响应头配置的5大挑战
挑战1:CSP策略复杂度爆炸
CSP有超过20个指令,指令之间有继承和覆盖关系。一条配置不当就可能让整个策略形同虚设——比如在script-src中使用'unsafe-inline'或'unsafe-eval',基本等于没有CSP。
挑战2:HSTS配置不当导致"自锁"
HSTS的max-age一旦设置过长,在证书过期或更换域名时,用户将无法访问网站。preload列表更是不可逆——从列表中移除需要数周时间。
挑战3:CORS配置"一刀切"
很多开发者为了方便,直接设置Access-Control-Allow-Origin: *,这在携带凭证的请求中根本不生效,反而暴露了开发者对CORS机制的理解不足。
挑战4:Permissions-Policy与业务冲突
禁用geolocation、camera等特性时,可能影响正常业务功能。如何在安全与可用性之间找到平衡点,需要逐页面精细配置。
挑战5:多服务器环境的一致性
Nginx、Caddy、Cloudflare Workers、Vercel Edge——不同环境的安全头配置方式各异,确保策略在所有入口一致生效是生产环境的核心难题。
5种生产模式:从CSP到HSTS的安全响应头实战
模式1:Content-Security-Policy(CSP)——指令配置、Nonce与Report-Only
CSP是防御XSS最有效的响应头。通过白名单机制控制页面能加载哪些资源,从源头切断恶意代码注入。
基础指令配置
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests
| 指令 | 含义 | 推荐值 | 说明 |
|---|---|---|---|
default-src |
所有资源的默认策略 | 'self' |
其他指令未指定时继承此值 |
script-src |
JavaScript来源 | 'self' 'nonce-xxx' |
永远不要用'unsafe-inline' |
style-src |
CSS来源 | 'self' 'unsafe-inline' |
内联样式通常不可避免 |
img-src |
图片来源 | 'self' data: https: |
允许data URI和HTTPS图片 |
font-src |
字体来源 | 'self' https://fonts.gstatic.com |
限制字体CDN |
connect-src |
fetch/XHR/WebSocket来源 | 'self' https://api.example.com |
限制API调用目标 |
frame-ancestors |
嵌套来源 | 'none' |
替代X-Frame-Options |
base-uri |
<base>标签来源 |
'self' |
防止base劫持 |
form-action |
表单提交目标 | 'self' |
防止表单劫持 |
upgrade-insecure-requests |
自动升级HTTP为HTTPS | (无值) | 配合HSTS使用 |
Nonce-based CSP——生产环境最佳实践
'unsafe-inline'会让CSP形同虚设。使用Nonce(一次性随机数)替代,只允许带正确Nonce的内联脚本执行。
import crypto from "crypto";
function generateNonce(): string {
return crypto.randomBytes(16).toString("base64");
}
function applyCSPNonce(res: any, html: string): string {
const nonce = generateNonce();
res.setHeader(
"Content-Security-Policy",
[
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: https:`,
`font-src 'self' https://fonts.gstatic.com`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
].join("; ")
);
return html.replace(/<script>/g, `<script nonce="${nonce}">`);
}
CSP Report-Only——安全上线策略
直接上线CSP可能破坏现有功能。先用Content-Security-Policy-Report-Only收集违规报告,确认无误后再切换为强制模式。
interface CSPViolationReport {
documentUri: string;
referrer: string;
blockedUri: string;
violatedDirective: string;
effectiveDirective: string;
originalPolicy: string;
disposition: "enforce" | "report";
statusCode: number;
}
function setupCSPReportOnly(res: any): void {
res.setHeader(
"Content-Security-Policy-Report-Only",
[
`default-src 'self'`,
`script-src 'self' 'nonce-placeholder'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: https:`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`report-uri /api/csp-report`,
].join("; ")
);
}
import express from "express";
const app = express();
app.use(express.json({ type: "application/csp-report" }));
app.post("/api/csp-report", (req, res) => {
const report: CSPViolationReport = req.body["csp-report"];
console.warn("[CSP Violation]", {
blockedUri: report.blockedUri,
violatedDirective: report.violatedDirective,
documentUri: report.documentUri,
});
res.status(204).end();
});
模式2:HSTS与证书安全——Preload、子域名与Max-Age策略
HSTS告诉浏览器"只能通过HTTPS访问此站点",防止SSL剥离攻击和协议降级。
HSTS三级配置
Strict-Transport-Security: max-age=31536000
Strict-Transport-Security: max-age=31536000; includeSubDomains
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
| 配置级别 | 指令 | 效果 | 风险 | 适用场景 |
|---|---|---|---|---|
| Level 1 | max-age=31536000 |
当前域名1年内只走HTTPS | 低 | 初次部署 |
| Level 2 | + includeSubDomains |
所有子域名也走HTTPS | 中 | 确认所有子域名支持HTTPS |
| Level 3 | + preload |
加入浏览器内置HSTS列表 | 高 | 大型生产站点 |
HSTS渐进式部署策略
class HSTSDeploymentManager {
private currentStage: number = 1;
private readonly stages = [
{ maxAge: 300, includeSubDomains: false, preload: false, description: "5分钟测试" },
{ maxAge: 86400, includeSubDomains: false, preload: false, description: "1天验证" },
{ maxAge: 604800, includeSubDomains: false, preload: false, description: "1周稳定" },
{ maxAge: 2592000, includeSubDomains: false, preload: false, description: "30天确认" },
{ maxAge: 31536000, includeSubDomains: false, preload: false, description: "1年正式" },
{ maxAge: 31536000, includeSubDomains: true, preload: false, description: "子域名覆盖" },
{ maxAge: 31536000, includeSubDomains: true, preload: true, description: "Preload提交" },
];
getCurrentHeader(): string {
const stage = this.stages[this.currentStage - 1];
const parts = [`max-age=${stage.maxAge}`];
if (stage.includeSubDomains) parts.push("includeSubDomains");
if (stage.preload) parts.push("preload");
return parts.join("; ");
}
promote(): { success: boolean; message: string } {
if (this.currentStage >= this.stages.length) {
return { success: false, message: "已达到最高级别" };
}
this.currentStage++;
const stage = this.stages[this.currentStage - 1];
return { success: true, message: `升级到Stage ${this.currentStage}: ${stage.description}` };
}
getDeploymentStatus(): object {
const stage = this.stages[this.currentStage - 1];
return {
currentStage: this.currentStage,
totalStages: this.stages.length,
header: this.getCurrentHeader(),
description: stage.description,
nextStep: this.currentStage < this.stages.length
? this.stages[this.currentStage].description
: "已完成全部部署",
};
}
}
const hstsManager = new HSTSDeploymentManager();
console.log(hstsManager.getCurrentHeader());
console.log(hstsManager.promote());
console.log(hstsManager.getDeploymentStatus());
HTTP到HTTPS重定向+HSTS
server {
listen 80;
server_name example.com *.example.com;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.com.pem;
ssl_certificate_key /etc/ssl/private/example.com.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
location / {
proxy_pass http://127.0.0.1:3000;
}
}
模式3:CORS配置——Preflight、凭证与通配符管理
CORS不是安全漏洞,而是浏览器的跨域访问控制机制。配置不当会导致API无法访问或安全边界被突破。
CORS核心概念
| 概念 | 说明 |
|---|---|
| 简单请求 | GET/HEAD/POST,仅允许特定Content-Type,不会触发preflight |
| 预检请求 | 浏览器先发OPTIONS请求,询问服务器是否允许跨域 |
| 凭证请求 | 携带Cookie的跨域请求,Access-Control-Allow-Origin不能为* |
| 通配符 | *匹配任意源,但不能与credentials: true同时使用 |
生产级CORS中间件
import { Request, Response, NextFunction } from "express";
interface CORSConfig {
allowedOrigins: string[];
allowedMethods: string[];
allowedHeaders: string[];
exposedHeaders: string[];
allowCredentials: boolean;
maxAge: number;
}
class CORSMiddleware {
private config: CORSConfig;
constructor(config: Partial<CORSConfig> = {}) {
this.config = {
allowedOrigins: config.allowedOrigins || ["https://example.com"],
allowedMethods: config.allowedMethods || ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: config.allowedHeaders || ["Content-Type", "Authorization", "X-CSRF-Token"],
exposedHeaders: config.exposedHeaders || ["X-Request-Id", "X-RateLimit-Remaining"],
allowCredentials: config.allowCredentials ?? true,
maxAge: config.maxAge || 86400,
};
}
handle = (req: Request, res: Response, next: NextFunction): void => {
const origin = req.headers.origin as string;
if (!origin) {
next();
return;
}
const isAllowed = this.config.allowedOrigins.some((allowed) => {
if (allowed.startsWith("*.") && origin.includes(allowed.slice(1))) {
return true;
}
return allowed === origin;
});
if (!isAllowed) {
res.status(403).json({ error: "CORS origin not allowed" });
return;
}
res.setHeader("Access-Control-Allow-Origin", origin);
if (this.config.allowCredentials) {
res.setHeader("Access-Control-Allow-Credentials", "true");
}
res.setHeader(
"Access-Control-Allow-Methods",
this.config.allowedMethods.join(", ")
);
res.setHeader(
"Access-Control-Allow-Headers",
this.config.allowedHeaders.join(", ")
);
res.setHeader(
"Access-Control-Expose-Headers",
this.config.exposedHeaders.join(", ")
);
res.setHeader("Access-Control-Max-Age", this.config.maxAge.toString());
if (req.method === "OPTIONS") {
res.status(204).end();
return;
}
next();
};
}
const corsMiddleware = new CORSMiddleware({
allowedOrigins: [
"https://example.com",
"https://app.example.com",
"*.example.dev",
],
allowedMethods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization", "X-CSRF-Token"],
allowCredentials: true,
maxAge: 86400,
});
app.use(corsMiddleware.handle);
CORS常见错误排查
class CORSTroubleshooter {
diagnose(req: Request, res: Response): string[] {
const issues: string[] = [];
const origin = req.headers.origin as string;
const acaoHeader = res.getHeader("Access-Control-Allow-Origin") as string;
const acacHeader = res.getHeader("Access-Control-Allow-Credentials") as string;
if (!origin) {
issues.push("缺少Origin头:非浏览器请求或同源请求,CORS不适用");
}
if (acaoHeader === "*" && acacHeader === "true") {
issues.push("致命错误:Allow-Origin为*时不能设置Allow-Credentials为true");
}
if (req.method === "OPTIONS" && res.statusCode !== 204 && res.statusCode !== 200) {
issues.push(`Preflight请求返回了${res.statusCode},应为204或200`);
}
const requestHeaders = req.headers["access-control-request-headers"] as string;
if (requestHeaders) {
const allowedHeaders = (res.getHeader("Access-Control-Allow-Headers") as string || "").split(", ");
const requested = requestHeaders.split(", ");
const missing = requested.filter((h) => !allowedHeaders.includes(h.toLowerCase()));
if (missing.length > 0) {
issues.push(`请求头${missing.join(", ")}未被Allow-Headers允许`);
}
}
return issues;
}
}
模式4:Permissions-Policy与功能控制——地理位置、摄像头、麦克风
Permissions-Policy(原Feature-Policy)控制页面可以使用哪些浏览器功能。即使被XSS攻击,攻击者也无法调用被禁用的API。
基础指令配置
Permissions-Policy: camera=(), microphone=(), geolocation=(self "https://map.example.com"), payment=(self), usb=(), magnetometer=(), gyroscope=(), fullscreen=(self), sync-xhr=(self), document-domain=()
| 指令 | 含义 | 推荐值 | 说明 |
|---|---|---|---|
camera |
摄像头 | () 禁用 |
除非视频会议场景 |
microphone |
麦克风 | () 禁用 |
除非语音输入场景 |
geolocation |
地理位置 | (self) 仅同源 |
地图应用可放开 |
payment |
支付API | (self) 仅同源 |
电商场景需要 |
usb |
WebUSB | () 禁用 |
极少场景需要 |
magnetometer |
磁力计 | () 禁用 |
极少场景需要 |
gyroscope |
陀螺仪 | () 禁用 |
游戏场景可放开 |
fullscreen |
全屏 | (self) |
视频播放需要 |
sync-xhr |
同步XHR | () 禁用 |
性能杀手,应禁用 |
document-domain |
document.domain | () 禁用 |
防止域放松攻击 |
按页面精细配置
import { Request, Response, NextFunction } from "express";
interface PagePermissionPolicy {
path: string;
policy: Record<string, string>;
}
class PermissionsPolicyManager {
private globalPolicy: Record<string, string> = {
camera: "()",
microphone: "()",
geolocation: "(self)",
payment: "(self)",
usb: "()",
magnetometer: "()",
gyroscope: "()",
fullscreen: "(self)",
sync_xhr: "()",
document_domain: "()",
};
private pageOverrides: PagePermissionPolicy[] = [
{
path: "/video-call",
policy: { camera: "(self)", microphone: "(self)" },
},
{
path: "/map",
policy: { geolocation: "(self https://map.example.com)" },
},
{
path: "/checkout",
policy: { payment: "(self https://pay.example.com)" },
},
];
getPolicyHeader(path: string): string {
let effectivePolicy = { ...this.globalPolicy };
for (const override of this.pageOverrides) {
if (path.startsWith(override.path)) {
effectivePolicy = { ...effectivePolicy, ...override.policy };
}
}
return Object.entries(effectivePolicy)
.map(([key, value]) => `${key}=${value}`)
.join(", ");
}
middleware = (req: Request, res: Response, next: NextFunction): void => {
const policyHeader = this.getPolicyHeader(req.path);
res.setHeader("Permissions-Policy", policyHeader);
next();
};
}
const permissionsPolicy = new PermissionsPolicyManager();
app.use(permissionsPolicy.middleware);
前端检测功能权限
async function checkPermission(featureName) {
try {
const result = await navigator.permissions.query({ name: featureName });
return {
feature: featureName,
state: result.state,
allowed: result.state !== "denied",
};
} catch {
return {
feature: featureName,
state: "unsupported",
allowed: false,
};
}
}
async function auditPagePermissions() {
const features = [
"camera",
"microphone",
"geolocation",
"notifications",
"persistent-storage",
];
const results = await Promise.all(features.map(checkPermission));
return results;
}
auditPagePermissions().then(console.table);
模式5:生产级安全响应头管理——Nginx/Caddy配置、自动化测试与监控
将所有安全响应头统一管理,确保在所有入口点一致生效,并建立自动化测试和监控体系。
完整安全响应头清单
interface SecurityHeadersConfig {
contentSecurityPolicy: string;
strictTransportSecurity: string;
permissionsPolicy: string;
referrerPolicy: string;
xContentTypeOptions: string;
xFrameOptions: string;
xXSSProtection: string;
crossOriginOpenerPolicy: string;
crossOriginEmbedderPolicy: string;
crossOriginResourcePolicy: string;
}
class SecurityHeadersManager {
private config: SecurityHeadersConfig;
constructor(domain: string, apiDomain: string, nonce?: string) {
const scriptSrc = nonce ? `'self' 'nonce-${nonce}'` : "'self'";
this.config = {
contentSecurityPolicy: [
`default-src 'self'`,
`script-src ${scriptSrc}`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: https:`,
`font-src 'self' https://fonts.gstatic.com`,
`connect-src 'self' ${apiDomain}`,
`frame-ancestors 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`upgrade-insecure-requests`,
].join("; "),
strictTransportSecurity: "max-age=31536000; includeSubDomains; preload",
permissionsPolicy: [
"camera=()",
"microphone=()",
"geolocation=(self)",
"payment=(self)",
"usb=()",
"fullscreen=(self)",
"sync-xhr=()",
].join(", "),
referrerPolicy: "strict-origin-when-cross-origin",
xContentTypeOptions: "nosniff",
xFrameOptions: "DENY",
xXSSProtection: "0",
crossOriginOpenerPolicy: "same-origin",
crossOriginEmbedderPolicy: "require-corp",
crossOriginResourcePolicy: "same-origin",
};
}
applyHeaders(res: any): void {
res.setHeader("Content-Security-Policy", this.config.contentSecurityPolicy);
res.setHeader("Strict-Transport-Security", this.config.strictTransportSecurity);
res.setHeader("Permissions-Policy", this.config.permissionsPolicy);
res.setHeader("Referrer-Policy", this.config.referrerPolicy);
res.setHeader("X-Content-Type-Options", this.config.xContentTypeOptions);
res.setHeader("X-Frame-Options", this.config.xFrameOptions);
res.setHeader("X-XSS-Protection", this.config.xXSSProtection);
res.setHeader("Cross-Origin-Opener-Policy", this.config.crossOriginOpenerPolicy);
res.setHeader("Cross-Origin-Embedder-Policy", this.config.crossOriginEmbedderPolicy);
res.setHeader("Cross-Origin-Resource-Policy", this.config.crossOriginResourcePolicy);
}
getNginxConfig(): string {
return `# Nginx Security Headers Configuration
add_header Content-Security-Policy "${this.config.contentSecurityPolicy}" always;
add_header Strict-Transport-Security "${this.config.strictTransportSecurity}" always;
add_header Permissions-Policy "${this.config.permissionsPolicy}" always;
add_header Referrer-Policy "${this.config.referrerPolicy}" always;
add_header X-Content-Type-Options "${this.config.xContentTypeOptions}" always;
add_header X-Frame-Options "${this.config.xFrameOptions}" always;
add_header X-XSS-Protection "${this.config.xXSSProtection}" always;
add_header Cross-Origin-Opener-Policy "${this.config.crossOriginOpenerPolicy}" always;
add_header Cross-Origin-Embedder-Policy "${this.config.crossOriginEmbedderPolicy}" always;
add_header Cross-Origin-Resource-Policy "${this.config.crossOriginResourcePolicy}" always;`;
}
getCaddyConfig(): string {
return `# Caddy Security Headers Configuration
header {
Content-Security-Policy "${this.config.contentSecurityPolicy}"
Strict-Transport-Security "${this.config.strictTransportSecurity}"
Permissions-Policy "${this.config.permissionsPolicy}"
Referrer-Policy "${this.config.referrerPolicy}"
X-Content-Type-Options "${this.config.xContentTypeOptions}"
X-Frame-Options "${this.config.xFrameOptions}"
X-XSS-Protection "${this.config.xXSSProtection}"
Cross-Origin-Opener-Policy "${this.config.crossOriginOpenerPolicy}"
Cross-Origin-Embedder-Policy "${this.config.crossOriginEmbedderPolicy}"
Cross-Origin-Resource-Policy "${this.config.crossOriginResourcePolicy}"
}`;
}
}
const headersManager = new SecurityHeadersManager(
"example.com",
"https://api.example.com"
);
console.log(headersManager.getNginxConfig());
自动化安全头测试
interface HeaderTestResult {
header: string;
present: boolean;
value: string | null;
passed: boolean;
recommendation: string;
}
class SecurityHeadersTester {
private requiredHeaders: Array<{
name: string;
validator: (value: string | null) => { passed: boolean; recommendation: string };
}> = [
{
name: "Content-Security-Policy",
validator: (value) => {
if (!value) return { passed: false, recommendation: "必须配置CSP" };
if (value.includes("'unsafe-inline'") && !value.includes("'nonce-")) {
return { passed: false, recommendation: "script-src不应使用unsafe-inline,改用nonce" };
}
if (value.includes("'unsafe-eval'")) {
return { passed: false, recommendation: "不应使用unsafe-eval" };
}
return { passed: true, recommendation: "CSP配置合理" };
},
},
{
name: "Strict-Transport-Security",
validator: (value) => {
if (!value) return { passed: false, recommendation: "必须配置HSTS" };
const maxAge = parseInt(value.match(/max-age=(\d+)/)?.[1] || "0");
if (maxAge < 2592000) {
return { passed: false, recommendation: `max-age应≥2592000(30天),当前${maxAge}` };
}
return { passed: true, recommendation: "HSTS配置合理" };
},
},
{
name: "X-Content-Type-Options",
validator: (value) => {
if (value !== "nosniff") {
return { passed: false, recommendation: "应设置为nosniff" };
}
return { passed: true, recommendation: "配置正确" };
},
},
{
name: "X-Frame-Options",
validator: (value) => {
if (value !== "DENY" && value !== "SAMEORIGIN") {
return { passed: false, recommendation: "应设置为DENY或SAMEORIGIN" };
}
return { passed: true, recommendation: "配置正确" };
},
},
{
name: "Referrer-Policy",
validator: (value) => {
if (!value) return { passed: false, recommendation: "必须配置Referrer-Policy" };
const safeValues = [
"no-referrer",
"strict-origin",
"strict-origin-when-cross-origin",
];
if (!safeValues.includes(value)) {
return { passed: false, recommendation: `推荐使用strict-origin-when-cross-origin,当前${value}` };
}
return { passed: true, recommendation: "配置正确" };
},
},
{
name: "Permissions-Policy",
validator: (value) => {
if (!value) return { passed: false, recommendation: "应配置Permissions-Policy" };
return { passed: true, recommendation: "已配置" };
},
},
];
async testUrl(url: string): Promise<HeaderTestResult[]> {
const response = await fetch(url);
const headers = response.headers;
return this.requiredHeaders.map(({ name, validator }) => {
const value = headers.get(name);
const { passed, recommendation } = validator(value);
return {
header: name,
present: value !== null,
value,
passed,
recommendation,
};
});
}
generateReport(results: HeaderTestResult[]): string {
const passed = results.filter((r) => r.passed).length;
const total = results.length;
const score = Math.round((passed / total) * 100);
let report = `安全响应头评分: ${score}/100 (${passed}/${total} 通过)\n\n`;
for (const result of results) {
const icon = result.passed ? "✅" : "❌";
report += `${icon} ${result.header}\n`;
report += ` 值: ${result.value || "(未设置)"}\n`;
report += ` 建议: ${result.recommendation}\n\n`;
}
return report;
}
}
const tester = new SecurityHeadersTester();
tester.testUrl("https://example.com").then((results) => {
console.log(tester.generateReport(results));
});
CI/CD集成——每次部署自动检测
name: Security Headers Check
on:
deployment_status:
jobs:
check-headers:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm install
- name: Run security headers test
env:
TARGET_URL: ${{ github.event.deployment_status.environment_url }}
run: npx tsx scripts/check-security-headers.ts $TARGET_URL
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: security-headers-report
path: security-headers-report.json
监控与告警——安全头变更检测
interface HeaderSnapshot {
url: string;
timestamp: string;
headers: Record<string, string>;
}
class SecurityHeadersMonitor {
private snapshots: Map<string, HeaderSnapshot[]> = new Map();
private alertCallback: (message: string) => void;
constructor(alertCallback: (message: string) => void) {
this.alertCallback = alertCallback;
}
async check(url: string): Promise<void> {
const response = await fetch(url);
const currentHeaders: Record<string, string> = {};
const securityHeaderNames = [
"content-security-policy",
"strict-transport-security",
"permissions-policy",
"referrer-policy",
"x-content-type-options",
"x-frame-options",
"cross-origin-opener-policy",
];
for (const name of securityHeaderNames) {
const value = response.headers.get(name);
if (value) currentHeaders[name] = value;
}
const snapshot: HeaderSnapshot = {
url,
timestamp: new Date().toISOString(),
headers: currentHeaders,
};
const previous = this.snapshots.get(url);
if (previous && previous.length > 0) {
const lastSnapshot = previous[previous.length - 1];
this._detectChanges(url, lastSnapshot, snapshot);
}
if (!this.snapshots.has(url)) {
this.snapshots.set(url, []);
}
this.snapshots.get(url)!.push(snapshot);
}
private _detectChanges(url: string, previous: HeaderSnapshot, current: HeaderSnapshot): void {
const allKeys = new Set([
...Object.keys(previous.headers),
...Object.keys(current.headers),
]);
for (const key of allKeys) {
const prevValue = previous.headers[key];
const currValue = current.headers[key];
if (prevValue && !currValue) {
this.alertCallback(`[ALERT] ${url}: 安全头 ${key} 被移除!`);
} else if (!prevValue && currValue) {
this.alertCallback(`[INFO] ${url}: 新增安全头 ${key}`);
} else if (prevValue !== currValue) {
if (this._isWeakened(key, prevValue!, currValue!)) {
this.alertCallback(`[WARNING] ${url}: 安全头 ${key} 被弱化!\n 旧值: ${prevValue}\n 新值: ${currValue}`);
}
}
}
}
private _isWeakened(headerName: string, oldValue: string, newValue: string): boolean {
if (headerName === "strict-transport-security") {
const oldMaxAge = parseInt(oldValue.match(/max-age=(\d+)/)?.[1] || "0");
const newMaxAge = parseInt(newValue.match(/max-age=(\d+)/)?.[1] || "0");
return newMaxAge < oldMaxAge;
}
if (headerName === "content-security-policy") {
if (!oldValue.includes("'unsafe-inline'") && newValue.includes("'unsafe-inline'")) {
return true;
}
if (!oldValue.includes("'unsafe-eval'") && newValue.includes("'unsafe-eval'")) {
return true;
}
}
return false;
}
}
const monitor = new SecurityHeadersMonitor((msg) => console.log(msg));
setInterval(() => {
monitor.check("https://example.com");
}, 300000);
5大常见陷阱
陷阱1:CSP中使用'unsafe-inline'
❌ Content-Security-Policy: script-src 'self' 'unsafe-inline'
✅ Content-Security-Policy: script-src 'self' 'nonce-a1b2c3d4'
'unsafe-inline'允许所有内联脚本执行,CSP对XSS的防御完全失效。必须使用nonce或hash替代。
陷阱2:HSTS直接设置max-age=31536000
❌ Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
✅ Strict-Transport-Security: max-age=300 (先测试5分钟)
直接设置1年,一旦HTTPS出问题,用户将无法访问数周。必须从短时间开始逐步递增。
陷阱3:CORS配置Access-Control-Allow-Origin: *配合凭证
❌ Access-Control-Allow-Origin: *
❌ Access-Control-Allow-Credentials: true
浏览器规范明确禁止这种组合。携带凭证时必须指定具体Origin,不能使用通配符。
陷阱4:Nginx中add_header不使用always
❌ add_header X-Frame-Options DENY;
✅ add_header X-Frame-Options DENY always;
不加always,当响应状态码为404/500时,安全头不会被添加,攻击者可利用错误页面绕过防护。
陷阱5:忽略Cross-Origin隔离头
❌ 缺少 Cross-Origin-Opener-Policy 和 Cross-Origin-Embedder-Policy
✅ Cross-Origin-Opener-Policy: same-origin
✅ Cross-Origin-Embedder-Policy: require-corp
缺少COOP/COEP头,页面无法使用SharedArrayBuffer等高级API,且易受Spectre类侧信道攻击。
错误排查表
| 错误现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 内联脚本不执行 | CSP缺少nonce | 检查script-src是否包含nonce值 | 为每个请求生成唯一nonce并注入 |
| CDN资源加载失败 | CSP未允许CDN域名 | 查看浏览器Console的CSP报错 | 在对应指令中添加CDN域名 |
| HTTPS页面仍可HTTP访问 | HSTS未生效 | 检查是否在HTTPS响应中返回HSTS头 | HSTS只在HTTPS响应中生效 |
| CORS预检请求失败 | OPTIONS未正确响应 | 检查服务端是否处理OPTIONS方法 | 返回204并设置正确的CORS头 |
credentials: true报错 |
Allow-Origin为* |
检查CORS响应头组合 | 改为具体Origin值 |
| API跨域请求被拒 | 缺少Access-Control-Allow-Headers | 检查请求头是否在Allow-Headers中 | 添加自定义请求头到白名单 |
| 地理位置API不可用 | Permissions-Policy禁用 | 检查geolocation指令配置 | 在需要的页面放开geolocation权限 |
| SharedArrayBuffer不可用 | 缺少COOP/COEP头 | 检查Cross-Origin隔离头 | 添加COOP: same-origin和COEP: require-corp |
| 安全头在错误页面缺失 | Nginx add_header缺少always | 检查404/500页面的响应头 | 添加always参数 |
| 子域名HTTP可访问 | HSTS未设置includeSubDomains | 检查HSTS指令 | 确认所有子域名支持HTTPS后添加 |
高级优化
优化1:CSP Report收集与分析
将CSP违规报告集中收集,建立安全态势感知。
import express from "express";
interface CSPReportPayload {
"csp-report": {
"document-uri": string;
referrer: string;
"violated-directive": string;
"effective-directive": string;
"original-policy": string;
disposition: string;
"blocked-uri": string;
"line-number": number;
"column-number": number;
"source-file": string;
"status-code": number;
};
}
class CSPReportCollector {
private reports: Array<{
timestamp: string;
blockedUri: string;
violatedDirective: string;
documentUri: string;
source: string;
}> = [];
handleReport = (req: express.Request, res: express.Response): void => {
const payload = req.body as CSPReportPayload;
const report = payload["csp-report"];
this.reports.push({
timestamp: new Date().toISOString(),
blockedUri: report["blocked-uri"],
violatedDirective: report["violated-directive"],
documentUri: report["document-uri"],
source: report["source-file"] || "unknown",
});
if (this.reports.length % 100 === 0) {
this._analyze();
}
res.status(204).end();
};
private _analyze(): void {
const directiveCounts: Record<string, number> = {};
const blockedUriCounts: Record<string, number> = {};
for (const report of this.reports) {
directiveCounts[report.violatedDirective] =
(directiveCounts[report.violatedDirective] || 0) + 1;
blockedUriCounts[report.blockedUri] =
(blockedUriCounts[report.blockedUri] || 0) + 1;
}
console.log("[CSP Analysis] Top violated directives:", directiveCounts);
console.log("[CSP Analysis] Top blocked URIs:", blockedUriCounts);
}
getTopViolations(limit: number = 10): Array<{
directive: string;
blockedUri: string;
count: number;
}> {
const counts: Record<string, number> = {};
for (const report of this.reports) {
const key = `${report.violatedDirective}|${report.blockedUri}`;
counts[key] = (counts[key] || 0) + 1;
}
return Object.entries(counts)
.sort(([, a], [, b]) => b - a)
.slice(0, limit)
.map(([key, count]) => {
const [directive, blockedUri] = key.split("|");
return { directive, blockedUri, count };
});
}
}
const cspCollector = new CSPReportCollector();
const reportApp = express();
reportApp.use(express.json({ type: ["application/json", "application/csp-report"] }));
reportApp.post("/api/csp-report", cspCollector.handleReport);
优化2:动态CSP——根据页面类型生成不同策略
interface PageCSPProfile {
scripts: string[];
styles: string[];
images: string[];
connect: string[];
frame: string[];
}
class DynamicCSPGenerator {
private profiles: Map<string, PageCSPProfile> = new Map([
[
"homepage",
{
scripts: ["'self'", "https://cdn.example.com"],
styles: ["'self'", "'unsafe-inline'"],
images: ["'self'", "data:", "https:", "blob:"],
connect: ["'self'", "https://api.example.com"],
frame: ["'none'"],
},
],
[
"dashboard",
{
scripts: ["'self'", "'nonce-{{NONCE}}'", "https://cdn.example.com", "https://charts.example.com"],
styles: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
images: ["'self'", "data:", "https:", "blob:"],
connect: ["'self'", "https://api.example.com", "wss://ws.example.com"],
frame: ["'self'", "https://embed.example.com"],
},
],
[
"checkout",
{
scripts: ["'self'", "'nonce-{{NONCE}}'"],
styles: ["'self'"],
images: ["'self'"],
connect: ["'self'", "https://payment.example.com"],
frame: ["https://payment.example.com"],
},
],
]);
generate(profileName: string, nonce: string): string {
const profile = this.profiles.get(profileName);
if (!profile) throw new Error(`Unknown profile: ${profileName}`);
const directives: string[] = [];
directives.push(`default-src 'self'`);
directives.push(`script-src ${profile.scripts.map((s) => s.replace("{{NONCE}}", nonce)).join(" ")}`);
directives.push(`style-src ${profile.styles.join(" ")}`);
directives.push(`img-src ${profile.images.join(" ")}`);
directives.push(`connect-src ${profile.connect.join(" ")}`);
directives.push(`frame-ancestors ${profile.frame.join(" ")}`);
directives.push(`base-uri 'self'`);
directives.push(`form-action 'self'`);
return directives.join("; ");
}
}
const cspGenerator = new DynamicCSPGenerator();
const homepageCSP = cspGenerator.generate("homepage", "placeholder");
const checkoutCSP = cspGenerator.generate("checkout", "a1b2c3d4");
console.log("Homepage CSP:", homepageCSP);
console.log("Checkout CSP:", checkoutCSP);
优化3:Expect-CT与证书透明度
Expect-CT: max-age=86400, enforce, report-uri="https://example.com/ct-report"
虽然Chrome已移除Expect-CT支持(2026年),但证书透明度(Certificate Transparency)仍是HTTPS安全的重要保障。确保你的证书通过CT日志签发:
class CTLogVerifier {
verifyCertificate(pemCertificate: string): {
ctCompliant: boolean;
scts: Array<{ logId: string; timestamp: number; signature: string }>;
} {
return {
ctCompliant: true,
scts: [
{ logId: "google-argon-2026", timestamp: Date.now(), signature: "verified" },
{ logId: "cloudflare-nimbus-2026", timestamp: Date.now(), signature: "verified" },
],
};
}
}
工具对比
| 特性 | Mozilla Observatory | SecurityHeaders.com | HSTS Preload | CSP Evaluator |
|---|---|---|---|---|
| 检测范围 | 全部安全头 | 全部安全头 | HSTS Preload状态 | CSP策略分析 |
| 评分机制 | A-F等级 | A-F等级 | 通过/未通过 | 安全建议 |
| CSP深度分析 | 基础 | 基础 | 不涉及 | 深度指令分析 |
| HSTS Preload检查 | ✅ | ✅ | ✅ 专属 | ❌ |
| API支持 | ✅ | ❌ | ✅ | ✅ |
| CI/CD集成 | ✅ | ❌ | ❌ | ✅ |
| 免费使用 | ✅ | ✅ | ✅ | ✅ |
| 适用场景 | 全面扫描 | 快速检查 | Preload提交 | CSP策略优化 |
推荐:生产环境使用Mozilla Observatory做全面扫描 + CSP Evaluator做策略优化 + HSTS Preload做Preload提交。
总结
安全响应头是Web安全的"性价比之王"——不需要改业务代码,只需几行服务器配置,就能防御XSS、点击劫持、协议降级、MIME嗅探等多种攻击。CSP是防御XSS的终极武器,HSTS是HTTPS的守护者,CORS是跨域访问的守门人,Permissions-Policy是浏览器功能的开关,而统一管理和自动化测试是确保这些头在生产环境持续生效的关键。
推荐工具
- 哈希计算 — 生成CSP的script-src hash值
- Base64编码/解码 — 生成CSP nonce和解析编码载荷
- HTTP状态码查询 — 排查CORS和安全头相关的HTTP错误
本站提供浏览器本地工具,免注册即可试用 →