CSP写了仍挡不住XSS?先删掉unsafe-inline

|作者: QUASA 编辑团队|2 分钟阅读| 1
CSP写了仍挡不住XSS?先删掉unsafe-inline

如果脚本策略仍依赖 'unsafe-inline',CSP 可能允许攻击者注入的内联 JavaScript 执行。要用 CSP 降低 XSS 风险,先为合法脚本配置 nonce 或哈希,再移除 'unsafe-inline';MDN 的 CSP 指南推荐这种严格策略,并说明 CSP 不能代替输入净化。

动态生成的 HTML 适合在每次响应中更新 nonce;静态交付的 HTML 适合使用脚本哈希。上线顺序是用 Report-Only 观察、收集并处理违规、切换脚本授权方式、启用强制执行,最后检查关键功能和实际拦截结果。

先找出削弱脚本限制的配置

检查浏览器实际收到的响应头,重点看 script-src;若未设置它,还要检查作为后备规则的 default-src。'unsafe-inline' 会放行内联脚本、事件处理器属性中的代码以及 javascript: URL,因此只看到 Content-Security-Policy 响应头,并不能说明注入的脚本已被有效限制。

也不要把 script-src 改成 'self' 或不断加长域名名单,就认为迁移完成。来源名单回答的是脚本可以从哪里加载,无法逐一确认页面中哪些脚本才是获准的入口;名单过宽,还会扩大获准加载的范围。严格策略应围绕被授权的脚本建立,而不是依赖一串越来越长的域名。

有一个影响排查的细节:当同一指令包含 nonce 或哈希时,支持该机制的浏览器会忽略其中的 'unsafe-inline'。所以策略里出现这个词,并不必然表示该浏览器仍放行所有内联代码;但迁移完成后仍应移除它,并检查目标浏览器的实际行为。直接删词而未处理原有脚本,则可能先让业务功能失效。

第一步:用 Report-Only 观察

先通过 HTTP 响应头发送候选策略,让浏览器记录本会被拦截的行为,同时维持页面现有运行状态。以下是动态页面的条件示例:Content-Security-Policy-Report-Only: default-src 'self'; script-src 'nonce-{RANDOM}' 'strict-dynamic'; style-src 'self'; img-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; report-uri /csp-report。{RANDOM} 只是占位符,服务端必须在响应头和获准的 script 标签中写入同一份响应专用的值。

Report-Only 必须通过响应头发送,不能写进 HTML 的 meta 标签。候选策略中的 style-src、img-src 和 connect-src 也应按页面实际使用的资源调整:图片违规应查图片来源,网络请求违规应查连接目标,不应为了消除这类报告而放宽 script-src。

第二步:收集违规并修复合法代码

让 /csp-report 接收报告,按页面、违反的指令和被拦截的资源分类,再关联到具体交互。报告可能带有页面 URL,收集端应限制接收量并控制保留范围。若使用较新的上报方式,可以通过 Reporting-Endpoints 定义端点并在策略中设置 report-to;为兼顾仍使用 report-uri 的浏览器,可以同时声明两者。

优先处理应用自己的内联事件处理器、javascript: URL 和依赖 eval() 的代码。例如,把 onclick 中的逻辑移入获准脚本,再用 addEventListener() 注册事件。还要梳理第三方组件加载了哪些后续脚本;仅看报告数量的增减,无法判断关键页面是否已经准备好启用拦截。

第三步:按页面交付方式授权脚本

web.dev 的严格 CSP 部署指南分别给出动态页面的 nonce 方案和静态页面的哈希方案。选择依据是 HTML 与响应头能否一起逐次更新,而不是 JavaScript 文件存放在哪个域名。两种方式都只应授权应用打算运行的脚本入口。

动态渲染:每份响应生成新 nonce

动态页面可使用这一条件示例:Content-Security-Policy: default-src 'self'; script-src 'nonce-{RANDOM}' 'strict-dynamic'; style-src 'self'; img-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; report-uri /csp-report。服务端用密码学安全的随机数生成器产生不可预测的值,以 Base64 编码替换 {RANDOM},并只给获准的标签写入匹配属性,例如 <script nonce="{RANDOM}" src="/app.js"></script>。同一响应中的获准脚本可以共用该值,下一份响应必须重新生成。

不要把 nonce 固定在构建产物里,也不要在最终 HTML 中批量给所有 script 标签补上 nonce;后者可能同时授权注入的标签。如果 CDN 缓存完整 HTML,应确认缓存命中时不会反复分发同一 nonce 和响应头;无法逐次更新时,改用哈希更合适。'strict-dynamic' 允许获准脚本加载后续脚本,因此还需检查入口脚本是否会用不可信输入拼接加载地址。

静态交付:从受控脚本生成哈希

静态页面可使用:Content-Security-Policy: default-src 'self'; script-src 'sha256-{BASE64_HASH}' 'strict-dynamic'; style-src 'self'; img-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; report-uri /csp-report。{BASE64_HASH} 应替换为获准内联脚本内容的 SHA-256 哈希经 Base64 编码后的结果;计算对象是标签之间的脚本内容,不包括 script 标签。每段需单独授权的内联脚本都要有对应哈希。

脚本内容即使只改动空白字符,也要重新计算哈希并更新策略。若通过哈希授权外部脚本,对应的 script 标签还需要匹配的 integrity 值。构建流程应从受控脚本生成这些值,并让 HTML 与响应头一同发布;不要把控制台给出的每个哈希直接加入策略,以免把应调查的代码列为可信入口。

第四步:启用强制执行

候选策略下的业务违规处理完毕后,把策略放入 Content-Security-Policy 响应头,浏览器才会实际拦截违规脚本。可先覆盖已完成迁移的页面,再检查应用、代理与 CDN 最终返回的响应头是否一致。原有 Report-Only 响应头仍可用于观察下一版候选策略,但它本身不会提供拦截。

强制执行时,核对页面是否仍需要所列的图片、样式和网络请求来源;上面的响应头是需按应用修改的示例,并非所有站点通用配置。尤其不要为了让某个组件恢复运行,就重新加入 'unsafe-inline' 或放开整类脚本来源:应先确定它依赖的具体加载方式。

第五步:回归检查拦截与页面功能

回归检查要同时覆盖获准脚本正常运行,以及没有有效 nonce 或哈希的内联脚本被阻止。登录、表单提交和编辑器等关键交互需要实际操作;还应确认违规报告送达,并检查模板发布、静态构建及缓存命中后的策略是否与页面匹配。组件升级后,应重新检查其加载链和相关交互,不能只凭报告数量判断配置有效。

严格 CSP 是 XSS 的额外防线。OWASP 的 CSP 速查表将它定位为纵深防御,并提醒开发者不要批量给所有 script 标签添加 nonce,因为注入的脚本也可能因此获得授权。应用仍须按数据进入 HTML、属性或 DOM 的具体位置进行安全处理;确需接收用户提交的 HTML 时,还要进行适当净化。

相关阅读:

分享:

订阅我们的新闻通讯

将最新 Web3、AI 和加密货币新闻直接发送到您的邮箱。

0