阅读声明:本文是资料研读与合成示例,整理于 2026-09-30。示例只针对本地或已获授权的测试服务,不对第三方域名发起探测;生产改动应走变更评审,保留旧配置和回滚窗口。

问题与适用边界

浏览器会根据响应头决定脚本、连接、嵌入、缓存和 Cookie 的行为,但服务器只能表达策略,不能替代输出编码、身份认证或服务端授权。审计前先画出边界:哪些响应包含个人数据,哪些页面需要第三方脚本,哪些接口只允许同源调用,哪些子域已经完全支持 HTTPS。没有这张清单,直接套用“最严格”模板,常见结果是登录循环、支付回调失败或客服系统无法嵌入。

本文重点放在可由响应验证的控制:CSP、HSTS、Cookie 属性和 Cache-Control。CORS、权限策略和框架级 CSRF 保护可以作为后续专题;它们的缺失不能仅凭本篇的“安全头数量”推断风险。

先固定可复核证据

审计记录应包含请求方法、最终 URL、重定向链、状态码、响应头、抓取时间和构建版本。不要只用 HEAD 判断,因为有些 CDN、应用中间件或 WAF 对 HEAD 和 GET 走不同分支。对本地服务,先分别获取 GET 和 HEAD,再把响应保存为构建产物中的证据文件:

# 仅针对本地合成服务
curl --fail-with-body --silent --show-error --location \
  --dump-header samples/web/headers-get.txt \
  --output samples/web/body-get.html http://127.0.0.1:8080/
curl --fail-with-body --silent --show-error --head \
  --dump-header samples/web/headers-head.txt \
  http://127.0.0.1:8080/

比较两份头时,先确认是否被重定向;再区分“缺少头”和“头在 304/错误页上不一致”。建议用规则输出证据片段而不是只输出 pass/fail。Secx 的 headerlint 是一个只读的合成项目示例,适合把这类规则接入 CI。

CSP:报告模式到 nonce

Content-Security-Policy(CSP)是浏览器执行限制的声明。第一次上线不应直接删除未知来源,而应先发送 Content-Security-Policy-Report-Only,收集真实页面触发的违规样本。MDN 明确区分了 report-only 与强制模式:前者记录或发送报告但不阻止资源,适合识别兼容性影响。

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-{{request_nonce}}';
  style-src 'self';
  img-src 'self' data:;
  connect-src 'self' https://api.example.test;
  frame-ancestors 'none';
  report-to csp-endpoint

这里的 {{request_nonce}} 必须由服务端为每次响应生成不可预测的随机值,并同时放入允许的内联 <script nonce="…">。不能把 nonce 写死在模板、日志或前端代码里,也不能把 'unsafe-inline' 当作长期修复。若页面有第三方分析脚本,先列出确切的来源和数据流,避免使用过宽的通配符。

报告接收端也属于攻击面:应限制请求体大小、去除个人数据、记录采样率和策略版本,并防止报告接口被当作高流量放大器。收到一条报告不等于存在可利用 XSS;浏览器扩展、旧缓存、浏览器预加载和测试插件都可能产生噪声。将报告按页面、指令、来源和部署版本聚合,确认重复出现后再收紧。

HSTS:强制 HTTPS 也要能回滚

Strict-Transport-Security 告诉浏览器在指定时间内把 HTTP 导航改写成 HTTPS。先从主域的短周期开始,例如 max-age=86400,确认所有入口、静态资源、健康检查和子域都能用 HTTPS,再逐步延长。includeSubDomains 会扩大影响范围,不能在遗留子域未盘点时盲目打开。

HSTS 并非“设置后不可逆”:服务器可以发送 max-age=0 让浏览器在下一次成功的 HTTPS 响应后停止继续记住策略;但已经缓存策略的用户在看到回滚响应前仍会强制 HTTPS,预加载列表还涉及浏览器发布周期。因此,预加载不是普通配置开关,应先完成官方预加载要求、域名盘点和故障演练。对内网、开发域名和会被 HTTP 健康检查访问的服务,要单独确认是否适合 HSTS。

# 稳定期的示例值;仅在所有子域完成 HTTPS 验证后使用
Strict-Transport-Security: max-age=31536000; includeSubDomains

会话 Cookie 至少应根据用途评估 Secure、HttpOnly、SameSite、Domain 和 Path。HttpOnly 能降低脚本直接读取 Cookie 的机会,却不能阻止浏览器自动携带它;SameSite=None 必须与 Secure 配对,并且只在确有跨站流程时使用。不要为了“看起来严格”把所有 Cookie 设成同一个值,先写清楚登录、OAuth 回调、嵌入式客服等业务路径。

缓存策略要和 Cookie 一起评审。含有账户、订单或令牌的响应通常应使用 Cache-Control: no-store,公共静态资源才适合长期共享缓存。private 只限制共享缓存,并不等于“永不落盘”;需要真正避免浏览器保存的场景要明确使用 no-store。同时检查 CDN 是否覆盖了源站头、是否按 Cookie 或 Authorization 变化缓存键。

Set-Cookie: __Host-session=REDACTED; Path=/; Secure; HttpOnly; SameSite=Lax
Cache-Control: no-store
Vary: Cookie, Authorization

误报、限制与修复验证清单

安全头检查器无法知道业务是否真的需要跨站嵌入,也无法证明服务端对象授权正确。常见误报包括:把仅在错误页缺失的头当成全站缺失、把第三方脚本报告当成 XSS、把短期 HSTS 当成“弱配置”,以及忽略反向代理覆盖头的事实。严重性应结合数据敏感度、浏览器覆盖率、攻击前置条件和修复成本,而不是由规则数量决定。

阶段验证问题可留存证据
基线GET、HEAD、重定向和错误页是否一致?原始头、URL、时间、构建号
试运行CSP report-only 是否按版本聚合,是否剔除噪声?报告样本、策略版本、采样率
上线登录、跨站回调、嵌入、静态资源是否可用?浏览器回归截图、自动化结果
回滚HSTS、Cookie 和 CDN 头是否有明确撤销步骤?变更单、max-age=0 预案、监控告警
  1. 记录每个头的目标、覆盖路径和责任人,不用“全站”替代真实路由范围。
  2. 在本地合成服务复现成功与失败两种响应,确保规则不会依赖网络偶然性。
  3. 部署后从真实浏览器验证 CSP、Cookie、缓存和重定向,而不是只看命令行输出。
  4. 为策略建立版本号和回滚说明;任何扩大来源范围的改动都重新评审。

相关项目与资料来源

本文参考并综合以下一手资料(访问/整理日期:2026-09-30):