外观
排查 CSP 与人机验证故障
这类故障的共同特点是症状离原因很远:最常见的一种在浏览器控制台里一条错误都不报。 按本表对症下药,不要凭直觉猜。
策略全文见 reference/csp.md,组件侧见 internals/captcha.md。
症状 → 原因速查
| 症状 | 最可能的原因 | 跳到 |
|---|---|---|
验证码转圈 20 秒后超时,Console 无任何 Refused to … | script-src 缺 'unsafe-eval' | A |
页面源码里能看到 __CSP_NONCE__ 原文 | nginx sub_filter 没生效 | B |
| 响应头 nonce ≠ 页面 nonce | 响应被缓存 | C |
| 三次请求 nonce 相同 | 同上 | C |
Console 报 Refused to load … cdn.jsdelivr.net | 用了 npm 版 Cap 产物 | D |
| Console 报被拦的、来路不明的内联脚本 | Rocket Loader 未关 | E |
| 登录页正常但控制台内某处功能连不上后端 | connect-src 未含该主机 | F |
A. 验证码静默超时
这是最难查的一种,因为主文档一条违规都不报。
Cap 的 instrumentation 挑战跑在 sandbox="allow-scripts" 的 srcdoc iframe 里, 那是个 opaque origin 的子文档——违规事件发生在那里,主文档看不到。表现只有一个: 控件转 20 秒然后报超时。
确认:
bash
curl -sI https://auth.natlan.io/authorize | grep -io "script-src[^;]*"看 script-src 里有没有 'unsafe-eval'。只给 'wasm-unsafe-eval' 不够—— PoW 解题器确实只需要后者,但 instrumentation 脚本用 eval / Function。
要直接看到那条违规,在 DevTools 里把 iframe 选作执行上下文,会看到 securitypolicyviolation script-src eval 紧接着 Instrumentation timeout。
修:把 'unsafe-eval' 加回 script-src。控制台改 apps/control/worker/index.js,授权页改 nginx 的 add_header Content-Security-Policy。
B. 占位符没被替换
只影响授权界面(控制台的 nonce 由 Worker 注入,没有占位符环节)。
bash
curl -s https://auth.natlan.io/authorize | grep -c __CSP_NONCE__
# 常态应为 0;出现 2 说明 sub_filter 没生效逐条排查:
nginx 有没有
http_sub_modulebashdocker compose -p docker-compose exec nginx nginx -V 2>&1 | tr ' ' '\n' | grep sub_modulegzip_static是不是开着 ——sub_filter改不动预压缩文件。sub_filter_once是不是 off ——index.html里有两处占位符,once on(默认)只会替换第一处。产物是不是旧的 —— 反过来,如果占位符本来就不在产物里,那是前端侧的问题:
index.html的占位符被删过。这正是迁往 Worker 那次留下的教训。bashgrep -c __CSP_NONCE__ apps/auth/dist/index.html # 必须是 2
C. nonce 不一致或不变
bash
curl -s -D /tmp/h.txt https://auth.natlan.io/authorize -o /tmp/b.html
grep -o "nonce-[a-f0-9]*" /tmp/h.txt | head -1
grep -o 'nonce="[a-f0-9]*"' /tmp/b.html | sort -u
for i in 1 2 3; do curl -sI https://auth.natlan.io/authorize | grep -io "nonce-[a-f0-9]*"; done两个值必须相同,三次请求必须互不相同。
原因几乎总是缓存:nonce 让每份文档唯一,缓存任何一份都会钉死一个过期 nonce。
- 授权页:
location = /authorize必须有add_header Cache-Control "no-store" always。 - 控制台:Worker 对 HTML 响应设
no-store,并且在转发给资产服务前删掉If-None-Match/If-Modified-Since——一个 304 会让浏览器继续用旧 HTML, 而 CSP 头是新的。
另一种可能:nonce 不是十六进制。base64 的 = padding 会截断不带引号的 HTML 属性, 表现为页面里的 nonce 比头里的短。
D. 出现 jsdelivr 请求
说明用回了 npm 发布版的 Cap 产物——它运行时会去 CDN 拉 WASM 解题器和 pako 兜底。
bash
grep -rl jsdelivr apps/auth/dist/ # 必须无输出
grep -r jsdelivr ui/vendor/cap # 必须无结果修:确认导入的是 ui/vendor/cap,不是 @cap.js/widget。若刚重新 vendor 过, 按 upgrade-cap-widget.md 把本地改动重新打回去。
E. 来路不明的内联脚本
Rocket Loader 会改写页面上的 <script> 标签,并注入一个属于它自己的、没有 nonce 的 内联脚本。nonce 方案对字节改写免疫,但对标签被整个改写无能为力。
修:在 natlan.io zone 上关闭 Rocket Loader。顺带确认 Auto Minify (HTML) 也是 off——它是历史上打掉哈希方案的元凶,虽然对 nonce 无害。
F. connect-src 拦掉了控制服务器
只影响控制台。登录页的「API Token」方式允许运维输入任意控制服务器地址, 但 CSP 的 connect-src 是一份固定允许清单。
bash
curl -sI https://console.natlan.io/ | grep -io "connect-src[^;]*"修:把新主机加进 apps/control/worker/index.js 的 connect-src 并重新部署。 这是有意的设计——地址可以输,但可达的主机由部署方控制。
通用手法
在真实浏览器里复现,不要只看 curl。 ④ 类验收(走完整授权流 / 登录流)自动化替代 不了:只有真的解一次题才知道验证码是否可用。
注意区分本站与浏览器扩展的报错。 content_main.js 之类的报错来自扩展,与站点无关。
回滚永远是先手。 授权页只需删掉 nginx 里的三行;控制台回退 Worker 到上一版部署。 带着故障排查比带着故障服务用户便宜。