为什么网站需要 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 作为输入校验、输出编码、权限控制和依赖安全之外的一层纵深防御。