KernelRanch
返回文章列表

安全响应头与 HSTS:用 Worker 一次配全

2026-11-10 #Cloudflare

Cloudflare 会自动加几个头:CF-RAY(请求标识,排查问题时报给客服或贴日志用)、NEL 与 Report-To(网络错误上报)、Server: cloudflare、Alt-Svc(声明 HTTP/3 可用)。这些是平台行为,你改不了。

真正决定浏览器行为的那几个头——Content-Security-Policy、Strict-Transport-Security、X-Frame-Options、Referrer-Policy、Permissions-Policy——不会自动出现。它们要么在源站加,要么在 Worker 里加。这篇走 Worker 这条路:代码里集中写一份,部署后抓响应头确认。

为什么放在 Worker:静态站没有源站配置可改,Pages 的 _headers 文件目前支持的头有限,而 Worker 里就是一个 Response 构造参数,改起来最直接。

1. 一个真实部署的 Worker

export default {
  async fetch(request) {
    const headers = {
      "content-type": "text/html; charset=utf-8",
      "strict-transport-security": "max-age=31536000; includeSubDomains",
      "x-content-type-options": "nosniff",
      "x-frame-options": "DENY",
      "referrer-policy": "strict-origin-when-cross-origin",
      "permissions-policy": "geolocation=(), microphone=(), camera=()",
      "cross-origin-opener-policy": "same-origin",
      "content-security-policy":
        "default-src 'self'; img-src 'self' data:; script-src 'self'; " +
        "style-src 'self'; frame-ancestors 'none'; base-uri 'self'"
    };
    return new Response("<!doctype html><title>headers demo</title><h1>ok</h1>", { headers });
  }
};

wrangler deploy 的输出:

Total Upload: 0.96 KiB / gzip: 0.52 KiB
Worker Startup Time: 3 ms
Uploaded cf83-security-headers (3.83 sec)
Deployed cf83-security-headers triggers (2.77 sec)
  https://cf83-security-headers.hyysn.workers.dev

抓回来的响应头(Invoke-WebRequest,200,92 字节响应体):

Alt-Svc: h3=":443"
CF-RAY: a58314f78d1fe2f7-HKG
Content-Security-Policy: default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self'; frame-ancestors 'none'; base-uri 'self'
Content-Type: text/html; charset=utf-8
cross-origin-opener-policy: same-origin
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
permissions-policy: geolocation=(), microphone=(), camera=()
Referrer-Policy: strict-origin-when-cross-origin
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[...]}
Server: cloudflare
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

一共 13 行头:8 行来自上面的代码(含 Content-Type),另外 5 行(Alt-Svc、CF-RAY、Nel、Report-To、Server)是 Cloudflare 自动加的,与代码无关。

2. 每个头解决什么问题

头作用建议值
Strict-Transport-Security让浏览器以后只走 HTTPSmax-age=31536000; includeSubDomains
Content-Security-Policy限制脚本、样式、图片、iframe 的来源从 default-src 'self' 起步,按需放行
X-Content-Type-Options禁止浏览器猜 MIME 类型nosniff
X-Frame-Options禁止被别站 iframe 嵌入DENY 或 SAMEORIGIN
Referrer-Policy控制跨站时带多少来源信息strict-origin-when-cross-origin
Permissions-Policy关闭不需要的浏览器能力geolocation=(), microphone=(), camera=()
Cross-Origin-Opener-Policy隔离窗口引用,防跨窗口攻击same-origin

X-Frame-Options 与 CSP 的 frame-ancestors 功能重叠,前者给老浏览器用,两个都写不冲突。X-XSS-Protection 已经不需要了,现代浏览器移除了这个过滤器,写上反而可能引入旧漏洞。

把这一组头写在 Worker 里还有个好处:可以按路径返回不同策略。/api/* 只需要 nosniff 与 HSTS,页面路由才需要完整 CSP。静态站也可以用 Pages 部署,_headers 文件能覆盖基础几项,但 CSP 这类需要按环境变化的头,Worker 更灵活,两种部署方式的差别见 72-pages-vs-workers-assets.md。

3. HSTS 的参数与上线顺序

Strict-Transport-Security: max-age=31536000; includeSubDomains 的意思是:一年内,这个域名的所有子域只能用 HTTPS 访问。浏览器记住之后,用户在地址栏输入 http:// 会被本地改写成 https://,连请求都不会发出去。

这就带来一个不可逆的问题:max-age 生效期间,如果某个子域的 HTTPS 挂了,用户连"降级到 HTTP"这个选项都没有。所以上线顺序反过来:

第一步,max-age=300 观察一天,确认所有子域都能 HTTPS。 第二步,改成 max-age=86400,观察一周。 第三步,加到一年,再考虑 includeSubDomains。 第四步,确认一年内不会再回退,才加 preload 并提交到浏览器预加载列表。

preload 是最难回退的一步:列表是编进浏览器安装包的,移除要等下一个版本。小站没有必要走这一步。

另外,HSTS 只在 HTTPS 响应里生效,HTTP 响应带的 HSTS 头会被浏览器忽略。开了 HSTS 之后,Always Use HTTPS 也应当打开,否则首次访问仍是 HTTP。

4. CSP 的写法与踩坑

CSP 是最容易把站点写坏的一个头。从最严格开始,逐步放行:

default-src 'self';
img-src 'self' data:;
script-src 'self';
style-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self'

四个常见坑:

内联脚本被拦。<script>...</script> 与 onclick="..." 都属于内联,script-src 'self' 不放行它们。要么把脚本抽成外部文件,要么用 nonce(每次请求生成随机值,写进 CSP 与 script 标签)。静态站更简单:全抽成外部文件。

内联样式被拦。style="..." 属性需要 style-src 'unsafe-inline',或者改用 class。很多 UI 库依赖内联样式,先看浏览器 console 的报错再决定。

第三方资源被拦。Google Fonts、统计脚本、Turnstile(接入方式见 81-turnstile.md)都要单独放行。Turnstile 需要 script-src 与 frame-src 里加 https://challenges.cloudflare.com,否则控件加载不出来。

上线前用 Content-Security-Policy-Report-Only 跑一周,只报告不拦截,看 console 与上报端点里的违规记录,确认没有遗漏再切成强制模式。

如果站点用了多线程 WASM 或 SharedArrayBuffer,还需要 Cross-Origin-Embedder-Policy: require-corp。这个头会连带影响所有跨域资源,加载图片、字体、iframe 都要带 CORP 头,加之前先确认依赖清单。

5. 验证

curl -sI https://example.com/ | grep -Ei 'strict-transport|content-security|x-frame|referrer'

四个头都在输出里就说明生效。注意 curl -I 发的是 HEAD 请求,如果 Worker 只对 GET 返回头,用 curl -s -D - -o /dev/null。

Cloudflare 控制台也能配一部分:SSL/TLS → Edge Certificates 里有 HSTS 开关(与 82-ssl-tls-modes.md 里的 Always Use HTTPS 同一个页面),Rules → Transform Rules 可以改响应头。控制台改的好处是不用维护代码,坏处是版本不可追溯、多环境难同步。纯静态站用控制台更省事;已经有 Worker 的站点,写在代码里更清楚。

常见问题

加了 CSP 之后页面白屏,怎么快速定位?

打开浏览器 console,CSP 违规会打印被拦的资源与指令名。临时把 Content-Security-Policy 改成 Content-Security-Policy-Report-Only,页面恢复可用的同时还能看到违规记录。

HSTS 开了之后能把网站改回 HTTP 吗?

在 max-age 到期前不能,浏览器不会发出 HTTP 请求。清浏览器 HSTS 缓存(chrome://net-internals/#hsts)只能解决你自己的机器,用户端无法操作。所以 max-age 从小到大加。

这些头对 SEO 有影响吗?

正面影响。HTTPS 与 HSTS 是排名因素之一,CSP 不直接影响排名但能减少被挂马的风险。要注意的是 Referrer-Policy 设得过严会丢失来源数据,strict-origin-when-cross-origin 是折中值。

小结

安全响应头是一组"配一次、长期生效"的设置。Cloudflare 只负责平台级的那几个,业务级的要自己写。Worker 里集中写一份是最省事的方式:改一处、所有路径生效、可进版本库。

顺序上先做 HSTS 的小 max-age 试跑,再做 CSP 的 Report-Only 观察,最后才收紧。这两个头都能把站点弄挂,而它们的收益是长期的、不会立刻看见的。

数据来源与检索时间

检索时间 2026-11-10。

来源URL对应数据
Cloudflare 定价页https://www.cloudflare.com/plans/免费计划包含 Universal SSL 与边缘证书设置
边缘证书文档https://developers.cloudflare.com/ssl/edge-certificates/HSTS 开关位置与参数
Workers 限额https://developers.cloudflare.com/workers/platform/limits/免费档请求额度(决定这套头的成本可忽略)

核验说明

  • Worker 为本机真实部署并已删除:wrangler deploy 输出、启动耗时、访问 URL 原样记录;响应头由 Invoke-WebRequest 实际抓取,状态码 200,响应体 92 字节,两处 JSON 路径(/ 与 /headers.json)都验证过。
  • 部署的 Worker 名为 cf83-security-headers,验证完执行 wrangler delete 删除,账号内未留资源。
  • 未执行:无 Zone 权限,控制台里的 HSTS 开关与 Transform Rules 未配置;CSP 的 Report-Only 观察流程未在真实站点跑过;HSTS 未在自有域名开启(避免把测试域名锁死)。
  • 未验证:各浏览器对 Permissions-Policy 的支持差异未逐一测试;文中建议值是按通用实践给的,不是抓包对照的结果。