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与业务冲突

禁用geolocationcamera等特性时,可能影响正常业务功能。如何在安全与可用性之间找到平衡点,需要逐页面精细配置。

挑战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是浏览器功能的开关,而统一管理和自动化测试是确保这些头在生产环境持续生效的关键。


推荐工具

本站提供浏览器本地工具,免注册即可试用 →

#Web安全头#CSP#HSTS#CORS#安全响应头#2026#前端安全