CSP 不该一次切到阻断:从 Report-Only 到 nonce 的上线契约
内容安全策略(CSP)能限制页面加载和执行代码的来源,降低脚本注入、恶意资源加载和页面被嵌入的风险。但它不是复制一段“最严格策略”就完成的安全头。页面还包含分析脚本、字体、图片、API、WebSocket、支付组件和旧式内联代码;直接切到阻断模式,最先被拦下的往往是正常功能。
可上线的 CSP 应被当成一份资源执行契约:先知道页面需要什么,再观测偏差,迁移危险用法,最后逐步阻断。
先画资源图,不要先写域名白名单
从浏览器网络记录、模板和构建产物中盘点 script、style、img、font、connect、frame、worker、媒体与表单目标。对每项记录来源、业务所有者、加载原因和删除条件。第三方域名不是永久许可;一个无法说明用途的来源,应先从页面移除。
default-src 只为部分资源获取指令提供回退,并不替代所有安全边界。base-uri、frame-ancestors 和 form-action 等需要明确声明。一个适合同源应用的起点可以是:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{PER_RESPONSE_RANDOM}';
style-src 'self';
img-src 'self' data:;
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
report-to csp-endpoint
这只是待验证的候选策略,不是通用答案。需要嵌入或被嵌入、使用 CDN 或提交到外部身份服务的产品必须逐项建模,不能为了消除告警加入 *、https: 或 'unsafe-inline'。
Report-Only 是迁移工具,不是完成状态
先通过 Content-Security-Policy-Report-Only 响应头部署候选策略。浏览器继续加载资源,但会在控制台显示违反项,并按 report-to 向 Reporting-Endpoints 指定的端点报告。考虑到浏览器兼容性,迁移期可以同时保留旧的 report-uri,但应清楚它已被替代。
报告不能直接变成白名单。扩展程序、浏览器注入、爬虫和攻击尝试都会制造噪声。接收端应限制请求大小和速率,把正文当成不可信输入,并按策略版本、页面、有效指令、被阻止源和浏览器聚合。启用 report-sample 时,样本可能带有用户数据;日志需要访问控制、脱敏和保留期限。
只报告也不能证明策略安全。它只覆盖实际访问过的路径。需要主动遍历登录、支付、错误页、懒加载、打印和低流量后台等流程,并在不同浏览器上执行。
动态页面用 nonce,静态页面优先用哈希或外部脚本
nonce 必须由密码学安全随机源为每个响应生成,W3C 建议编码前至少 128 位。响应头中的 'nonce-…' 与可信 script 标签的 nonce 属性必须一致:
<script nonce="" src="/assets/app.js"></script>
不要使用固定 nonce,也不要用中间件给所有脚本标签批量补 nonce;否则攻击者注入的标签也可能被授权。模板只能把 nonce 放到应用明确拥有的节点上。设置 base-uri 'none' 还能避免攻击者注入 base 后改变相对脚本地址的解析结果。
纯静态页面无法逐响应生成 nonce。最简单的方案是移除内联脚本并只加载同源文件;确实需要稳定的内联片段时,可以把其精确内容的 SHA-256 等哈希写入策略。空格或构建改写都会改变哈希,因此构建流程要自动计算并在内容变化时同步更新。
'strict-dynamic' 可以让带 nonce 或哈希的可信脚本继续加载它创建的非解析器脚本,适合某些模块加载器,但它扩大了可信代码的责任。若可信脚本把未验证 URL 交给动态脚本 API,攻击者仍可能借它加载代码。采用前要审计所有动态创建脚本的位置。
分层收紧,而不是等待“零报告”
可以先强制风险低且意图明确的 object-src 'none'、base-uri、frame-ancestors 与 form-action,同时让更严格的脚本策略保持 Report-Only。修复内联处理器和 eval(),再按页面或流量比例启用脚本阻断。旧的宽松强制策略与新的严格只报告策略可以同时存在,互不替代。
每次策略变更都要有版本号、负责人和回滚值。发布看板至少观察资源加载错误、前端异常、关键流程成功率和 CSP 违反趋势。回滚应恢复上一份已验证策略,而不是临时加 'unsafe-inline'。候选策略稳定后,再把相同值移入 Content-Security-Policy 响应头;Report-Only 随后可用于预演下一次收紧。
静态托管的 meta 有明确边界
无法控制响应头时,可以在文档靠前位置放置 <meta http-equiv="Content-Security-Policy">,且它只约束出现在它之后的内容。meta 不能提供 Report-Only,也不支持 report-uri、frame-ancestors 和 sandbox。因此它适合有限兜底,不等价于完整部署;需要点击劫持防护、集中报告和渐进上线时,应在 CDN、反向代理或托管平台配置响应头。
把策略写进回归测试
CI 可以扫描新出现的内联事件处理器、eval()、危险协议和未登记第三方源。浏览器测试则要同时证明两件事:正常路由、懒加载和表单仍可工作;插入的未授权内联脚本、外域脚本和 iframe 确实被拦截。测试还应读取最终响应头,因为应用、代理和 CDN 可能分别添加多份策略,而浏览器会同时执行它们。
上线前确认:资源图有负责人;策略从 Report-Only 获得真实流量证据;报告端限流且不泄露隐私;nonce 每响应唯一或静态内容使用可重复生成的哈希;危险回退没有为消除告警而放宽;关键路径经过跨浏览器测试;强制过程可分批且可恢复。
CSP 是纵深防御,不能替代输出编码、可信模板和输入处理。它真正的价值也不在策略字符串有多长,而在浏览器允许执行的能力有明确边界,并且这条边界能够被观测、测试和安全地改变。