加了 Content-Security-Policy 之后脚本全挂,该先动哪条指令?

刚把 Content-Security-Policy 写进响应头,首页就白了:统计脚本、聊天组件、自家打包出来的入口 JS 轮流在控制台报拒绝加载。第一反应经常是「先把 CSP 去掉让业务恢复」。MDN《Content Security Policy (CSP)》和 web.dev《Content security policy》都把 CSP 写成防 XSS 的纵深防御:用白名单限制脚本等资源从哪来,并限制内联脚本和 eval 一类接口。长期方案不该是永久关掉策略。

浏览器到底拦了什么

CSP 是一组用分号隔开的指令。跟脚本最相关的是 script-src。没单独写时,常会落到 default-src。来源可以是 'self'、具体主机、'none',以及 nonce / hash 等。页面上一旦存在 default-src 或 script-src,未额外放行的内联脚本、内联事件处理器、javascript: URL,以及 eval / new Function / 字符串版 setTimeout 等,默认都过不了。第三方统计若换了域名、或仍靠页面里塞 <script>…</script>,一开策略就全灭,很常见。

web.dev 用控制台报错说明白名单以外的脚本会被拒绝执行。排障时先打开开发者工具:拒绝加载的那一行会写出触犯了哪条 directive、被拦的是哪个 URL(或 inline)。对照 Network / 控制台里的 blocked 信息,比凭感觉把 script-src 改成 * 靠谱。过宽的域名白名单,两篇文档都提醒过,经常等于没防住 XSS。维护成本也会被第三方脚本域名清单拖垮。

策略应通过响应头下发,并且尽量覆盖所有要保护的响应,不只是首页 HTML。<meta http-equiv="Content-Security-Policy"> 能救急,但 MDN 写明它不支持全部能力(例如部分指令、Report-Only 也不能靠 meta 送)。

先 Report-Only,再 enforce

两篇文档都建议用报告模式摸清现状。响应头换成:

Content-Security-Policy-Report-Only: …

策略不会拦截,但会按你配置的上报地址(report-to / 仍可见的 report-uri 等)把违规打到服务端。MDN 还写:同一响应里可以同时带 Report-Only 和正式 Content-Security-Policy,前者只报、后者执行,方便在现行策略下试下一版。等报告里的域名、内联、eval 清得差不多了,再切回强制头。

上线强制策略时,优先收紧的是 script-src(以及作为兜底的 default-src),按实际需要加 'self'、可信 CDN、nonce/hash。MDN 介绍的严格 CSP 示例还会配上 object-src 'none'、base-uri 'none'。临时甩 'unsafe-inline' / 'unsafe-eval' 能救火,两篇文档都写明应尽快拿掉,否则 CSP 对 XSS 的意义会被掏空。nonce 或 hash 已经出现在指令里时,浏览器会忽略 'unsafe-inline',这点排错时也容易踩。

回滚顺序,而不是拆掉整扇门

脚本全挂时,可以按这个顺序收:

  • 控制台确认是哪条 directive、哪个资源。
  • 若尚未有报告通道,先改 Report-Only 或并行加一条 Report-Only,收集真实违规。
  • 只放行证实需要的脚本源。内联改外置文件,或改用 nonce/hash。事件改 addEventListener。模板引擎若还在运行时 eval 拼字符串,按文档思路换成预编译或 Trusted Types 相关路径,而不是长期挂着 'unsafe-eval'。
  • 不要把「删除整个 CSP 头」当成长期配置。业务恢复后仍应用白名单兜住注入面。

CSP 也管图片、样式、连接目标、嵌入方(frame-ancestors)等,但这次若只有脚本红,先盯 script-src / default-src,避免一次改十几个指令越改越乱。和跨域凭证那套头不是同一类问题:那边管 Cookie 带不带上,这边管脚本允不允许执行。

用户输入该清洗还是要清洗。CSP 是防漏的一层,文档也写了两者要一起用,不能互相替代。

若报告里反复出现同一条内联或某个第三方域名,先改那一处,再收紧下一刀。整站一次性「全部 'self'」往往只会换来另一轮白屏。

你们上 CSP 是一次 enforce 踩坑,还是先走了 Report-Only?统计和打点脚本最后是加域名,还是改成了 nonce?