首页
首页 / Web 安全 / OWASP

Content Security Policy:从 Report-Only 到逐步收紧

Web 安全 / OWASP 技术教程
预计阅读 13 分钟·最后更新 2026-10-21

为什么网站需要 Content Security Policy?

网站出现 XSS(跨站脚本)问题时,攻击者往往会想办法让浏览器执行本不应该执行的脚本。Content Security Policy,简称 CSP,是浏览器提供的一层额外安全约束:由网站声明哪些来源的脚本、样式、图片、字体、连接等内容可以被加载。

CSP 不能代替输入校验、输出编码、依赖升级和权限控制,但它可以显著缩小某些前端注入问题的攻击面。

实际部署 CSP 最容易犯的错误,是一上来就设置非常严格的策略,结果把登录、统计、验证码、编辑器或站内脚本一起拦掉。因此生产环境更适合采用“先观察,再收紧”的方式。

先理解一条 CSP 是什么意思

CSP 通常通过 HTTP 响应头下发。例如:

Content-Security-Policy: default-src ‘self’; img-src ‘self’ https: data:; object-src ‘none’

这条策略可以拆成三部分理解:

  • default-src ‘self’:没有单独声明规则的资源,默认只允许本站来源。
  • img-src ‘self’ https: data::图片允许本站、HTTPS 来源以及 data: URL。
  • object-src ‘none’:禁止 object、embed 等插件型内容。

浏览器在加载页面资源时会依据这些规则决定允许还是阻止。

为什么不建议第一次就直接启用严格 CSP?

一个已经运行很久的网站,往往存在大量真实依赖:主题脚本、WordPress 插件、CDN、统计服务、第三方字体、验证码、支付组件、内联样式甚至旧代码。

如果直接发布严格的 Content-Security-Policy,任何遗漏都可能变成真实故障。

因此更稳妥的第一步通常是使用 Report-Only。

第一阶段:先部署 Report-Only

Report-Only 的核心特点是:浏览器检查策略并产生违规信息,但通常不会因为该策略直接阻止资源。

Content-Security-Policy-Report-Only: default-src ‘self’; object-src ‘none’

这样可以先观察:如果未来真正启用这条策略,哪些资源会被拦截。

这一阶段的目标不是追求“零告警”,而是建立网站真实资源依赖清单。

Report-Only 阶段重点看什么?

浏览器开发者工具 Console 中可能出现 CSP violation 信息。你需要判断每条违规属于哪一种情况:

  • 网站真正需要的合法资源,但当前策略没有允许。
  • 旧主题或插件产生的内联脚本/内联样式。
  • 已经不再需要的第三方资源。
  • 浏览器扩展或本地环境产生的噪声。
  • 值得进一步调查的异常脚本来源。

不要看到一条违规就立刻把对应域名加入白名单。先确认它为什么存在。

default-src 为什么重要?

default-src 是很多 CSP 策略的默认回退规则。例如:

default-src ‘self’

意味着在没有更具体指令覆盖时,只允许从当前站点自身来源加载相关资源。

但现代网站通常还需要针对脚本、样式、图片、字体、网络连接等分别设置规则,因此实际策略会逐步拆分。

第二阶段:把不同资源类型拆开

经过一段时间观察后,可以开始为主要资源类型分别设置规则。

Content-Security-Policy-Report-Only:
default-src ‘self’;
script-src ‘self’;
style-src ‘self’;
img-src ‘self’ https: data:;
font-src ‘self’ https: data:;
connect-src ‘self’ https:;
object-src ‘none’;
base-uri ‘self’;

这仍然只是示意。生产站点必须根据自己的真实资源来源调整,而不是直接复制一份网上的 CSP。

script-src 是最需要谨慎处理的部分

script-src 控制 JavaScript 的来源,也是 CSP 部署中最容易导致网站功能异常的部分。

很多旧网站存在类似代码:

<script>window.appConfig = {…}</script>

如果策略只允许 script-src ‘self’,这类内联脚本可能不符合策略要求。

最简单的做法看似是加入 ‘unsafe-inline’,但这样会明显削弱 CSP 对脚本注入的防护价值。

nonce 是什么?

更现代的一种方案是给允许执行的内联脚本分配随机 nonce。

Content-Security-Policy: script-src ‘self’ ‘nonce-RANDOM_VALUE’

<script nonce=”RANDOM_VALUE”>
window.appConfig = {};
</script>

服务器每次响应生成不可预测的 nonce,并同时放入 CSP 响应头和被允许的 script 标签。只有 nonce 匹配的内联脚本才能执行。

关键点是 nonce 必须由服务器安全生成并按响应变化,不能把一个固定值长期写死。

hash 又是什么?

对于内容固定的内联脚本,也可以通过哈希值允许指定脚本执行。

script-src ‘self’ ‘sha256-…’

浏览器会对脚本内容进行校验,只有与策略中的哈希匹配时才允许执行。

这种方式适合内容稳定的脚本;如果脚本内容频繁变化,维护成本会增加。

为什么要尽量避免 ‘unsafe-eval’?

‘unsafe-eval’ 允许某些字符串形式的动态代码执行能力,例如 eval() 一类行为。它会削弱 CSP 的限制效果。

如果某个旧框架必须依赖它,可以先确认具体依赖来源并规划升级,而不是把 ‘unsafe-eval’ 当成默认配置长期保留。

style-src 为什么也经常出现大量告警?

WordPress 主题和插件经常生成内联 style 属性或 style 标签,因此严格的 style-src 可能比预期更容易产生违规。

处理方式与脚本类似:先通过 Report-Only 观察,再决定哪些样式可以迁移到外部样式表、哪些需要 nonce/hash,以及哪些旧代码应该清理。

不要为了消除 Console 告警就无条件扩大来源范围。

object-src ‘none’ 为什么经常推荐?

现代网站通常已经不依赖 Flash 等传统插件内容,因此:

object-src ‘none’

往往是一个风险较低、收益明确的收紧项。如果你的站点确实依赖 object/embed,则需要先验证实际业务。

base-uri 可以防什么?

HTML 的 base 元素可以改变页面中相对 URL 的解析基准。通过:

base-uri ‘self’

可以限制页面允许使用的 base URL 来源。对于不需要 base 标签的网站,也可以评估更严格的设置。

frame-ancestors 与点击劫持

frame-ancestors 用于控制哪些站点可以把当前页面嵌入 frame 或 iframe。

frame-ancestors ‘self’

如果网站完全不需要被其他页面嵌入,可以评估:

frame-ancestors ‘none’

这类策略有助于降低点击劫持风险。但如果站点有第三方后台、支付、嵌入式应用等场景,需要先确认兼容性。

第三阶段:开始从观察模式转向真正阻止

当 Report-Only 日志已经稳定,并且关键页面都经过验证后,可以选择一部分低风险规则先切换到正式 Content-Security-Policy。

例如先从这些规则开始:

  • object-src ‘none’
  • base-uri ‘self’
  • frame-ancestors 的明确限制
  • 已经确认来源的 img-src / font-src

script-src 和 style-src 往往最复杂,可以继续保留更长时间的观察和改造周期。

CSP 不要求一天之内达到最严格状态。逐步收紧比一次性上线后大面积回滚更适合生产环境。

WordPress 部署 CSP 为什么特别需要渐进式?

WordPress 的复杂之处在于主题和插件都可能向页面注入脚本、样式、iframe、字体和外部请求。更新插件以后,资源依赖还可能发生变化。

因此 WordPress 部署 CSP 时建议至少测试:

  • 首页和文章页
  • 登录与退出
  • WordPress 后台
  • 文章编辑器
  • 搜索、评论和表单
  • 验证码或第三方登录
  • 统计、广告或第三方组件
  • 移动端菜单和交互功能

不要只确认首页能打开就认为 CSP 已经部署完成。

Nginx 中可以怎么下发 CSP?

如果站点使用 Nginx,可以通过响应头配置 CSP。下面仅作为结构示例:

add_header Content-Security-Policy-Report-Only
“default-src ‘self’; object-src ‘none’; base-uri ‘self’;”
always;

实际修改前应先确认站点是否已经由 WordPress 插件、CDN、反向代理或其他 Nginx 配置下发 CSP,避免出现多个策略叠加后造成难以排查的问题。

修改 Nginx 后也应该先执行配置检查,再平滑重载服务。

如何检查网站当前有没有 CSP?

可以使用浏览器开发者工具,在 Network 中点击 HTML 主文档,然后查看 Response Headers。

也可以通过 curl 查看响应头:

curl -I https://example.com/

重点寻找:

content-security-policy:
content-security-policy-report-only:

如果两个头同时存在,要分别理解:正式 CSP 会实际执行阻止规则,而 Report-Only 用于观察报告。

如何判断 CSP 有没有把网站功能弄坏?

不要只看页面“能不能打开”。应该针对关键功能做回归测试。

  • 导航是否正常点击。
  • 登录和验证码是否正常。
  • 表单能否提交。
  • 图片、字体和图标是否完整。
  • 后台编辑器是否正常。
  • AJAX / REST API 请求是否成功。
  • 第三方登录、统计等必要服务是否工作。
  • 浏览器 Console 是否出现新的 CSP 阻止信息。

CSP 上线后,功能测试和安全策略验证必须同时进行。

常见错误一:直接复制别人的 CSP

不同网站依赖完全不同。别人允许的 CDN、统计服务、字体域名与你的网站没有必然关系。复制策略既可能造成故障,也可能加入大量根本不需要的白名单。

常见错误二:为了不报错加入大量 *

例如把来源放宽成任意域名,虽然 Console 可能安静了,但 CSP 的限制价值也会明显下降。策略应该基于实际需要逐项允许。

常见错误三:长期依赖 unsafe-inline

在迁移阶段,某些站点可能暂时离不开内联脚本或样式,但最终应该尽可能通过外部文件、nonce、hash 等方式减少对宽松规则的依赖。

常见错误四:只测试桌面首页

真正容易出问题的往往是登录、后台、移动菜单、弹窗、搜索、评论、支付或第三方组件。上线前应覆盖真实业务路径。

常见错误五:以为 CSP 能修复所有 XSS

CSP 是纵深防御措施,不是替代安全编码的万能方案。服务器端仍然必须正确处理输入、输出编码、权限校验、依赖安全和模板转义。

一套更稳妥的上线顺序

对于已经在线运行的网站,可以按照下面的节奏推进:

  • 第一步:盘点页面真实加载的脚本、样式、图片、字体和第三方连接。
  • 第二步:先发布 Content-Security-Policy-Report-Only。
  • 第三步:观察违规,区分必要依赖、历史遗留和异常来源。
  • 第四步:拆分 script-src、style-src、img-src、connect-src 等指令。
  • 第五步:逐步移除不必要的第三方来源和宽松规则。
  • 第六步:对内联脚本评估 nonce/hash 等方案。
  • 第七步:先将低风险规则切换为正式 CSP,并进行功能回归。
  • 第八步:继续收紧脚本策略,同时持续观察新版本和插件更新带来的变化。

总结

Content Security Policy 的价值,不在于写出一条看起来非常复杂的响应头,而在于让浏览器只信任网站真正需要的内容来源。

对于已经运行的生产网站,最稳妥的方式通常不是一次性开启严格策略,而是从 Report-Only 开始:先观察真实依赖,再逐步拆分规则、清理不必要来源,并对关键内联脚本采用 nonce 或 hash 等更精确的授权方式。

最终的目标应该是:在不破坏正常业务的前提下,让策略越来越明确、白名单越来越小,并把 CSP 作为输入校验、输出编码、权限控制和依赖安全之外的一层纵深防御。

发表评论