Web 安全

CSP (内容安全策略)

约 4 分钟阅读

什么是 CSP (Content Security Policy)

CSP (Content Security Policy) 是一种 HTTP 响应头,允许服务器控制网页可以加载资源 (脚本、样式表、图片、字体等) 的来源。由于浏览器会阻止加载或执行 CSP 不允许的资源,因此它作为阻止通过 XSS (跨站脚本攻击) 注入的脚本执行、并阻止窃取到的信息外发的一层发挥作用。

XSS 的根本防御措施是输出转义,但实现上的遗漏无法完全消除。CSP 作为纵深防御的一层,发挥着「即使存在 XSS 漏洞,也不让攻击者的脚本执行」的作用。不过它能阻止的只是「未被允许的加载与执行」,在允许范围之内成立的攻击无法防住。在安全头中它可指定的项目最多,能收紧到什么程度就直接决定了实际效果。规范方面,Level 2 于 2016 年 12 月 15 日成为 W3C 建议标准,Level 3 截至 2026 年 8 月仍作为 W3C 工作草案在制定中。

主要指令

CSP 由多个指令组成,为每种资源类型指定允许的来源。

  • default-src:其他指令未明确指定的资源的默认策略。default-src 'self' 仅允许来自同源的资源。回退只对加载类指令生效,base-uriform-actionframe-ancestors 即使写了 default-src 'none' 也仍处于未指定的状态,需要逐项单独指定
  • script-src:控制 JavaScript 的加载来源。XSS 防御的核心指令。避免使用 'unsafe-inline',推荐使用 nonce 或 hash。按照规范,一旦指定了 nonce 或 hash,'unsafe-inline' 就会被忽略,因此把两者并列书写只在为不解析 nonce 的旧实现保留兼容时才有意义
  • style-src:控制 CSS 的加载来源
  • img-src:控制图片的加载来源
  • connect-src:控制 fetch、XMLHttpRequest、WebSocket 的连接目标
  • font-src:控制 Web 字体的加载来源
  • frame-src:控制本页面的 iframe 中可以加载哪些提供方的页面 (嵌入一方的控制)
  • frame-ancestors:方向相反,控制哪些父页面源可以将本站嵌入 iframe (被嵌入一方的控制)。它是 X-Frame-Options 的后继,作为点击劫持的对策要指定的正是这一项

源值的指定方式

  • 'self':同源
  • 'none':全部阻止
  • 'nonce-{random}':仅允许具有特定 nonce 值的内联脚本/样式
  • 'strict-dynamic':也允许由通过 nonce 或 hash 许可的脚本动态加载的脚本。指定它之后,script-src 中并列的域名和协议以及 'self''unsafe-inline' 在判断脚本能否加载时都会被忽略,只有 nonce 和 hash 生效
  • 具体域名:https://cdn.example.com

分阶段部署步骤

CSP 的部署应循序渐进,避免破坏现有站点。

步骤 1:使用 Report-Only 模式监控

使用 Content-Security-Policy-Report-Only 头可以检测并报告策略违规,但不会阻止资源。首先使用此模式了解站点当前加载了哪些资源。

步骤 2:制定基础策略

根据报告分析结果,从图片、样式等加载类指令开始逐步收紧提供方。而对于脚本,2016 年的研究 (Lukas Weichselbaum 等「CSP Is Dead, Long Live CSP!」) 已经梳理出,留在已允许域名上的 JSONP 端点或开放重定向可能成为绕过的通道,因此设计上不能只依赖对提供方的列举 (白名单)。CSP Level 3 规范也作为参考信息提到,在 script-src 中把 nonce 或 hash 与 'strict-dynamic' 组合起来、并收紧 base-uri 的构成称为 Strict CSP,是易于实施的缓解方案。

步骤 3:生产环境部署与持续监控

切换到 Content-Security-Policy 头进行生产部署。通过 report-to 指令 (Level 3 中 report-uri 已不推荐,并给出了替换为前者的指引) 持续收集违规报告,应对误报和新的资源需求。过渡期把两者都写上,即使在只解析其中一种的环境里也不会漏掉报告。

推荐的严格策略示例

default-src 'none'; script-src 'self' 'nonce-{random}' 'strict-dynamic'; style-src 'self' 'nonce-{random}'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'

此策略通过 nonce 控制内联脚本,并使用 'strict-dynamic' 允许动态加载的脚本。如果需要兼容不解析 'strict-dynamic' 的浏览器,规范的注记中给出了在 script-src 中同时写上 https: 的做法 (在能够解析它的浏览器一侧会被忽略)。

CSP 与其他安全头的协同

CSP 能够判断的只有资源是否可以加载并执行。与其他安全头组合起来,让各自补上它所负责的范围。

  • HSTS 配合:强制 HTTPS。以明文 HTTP 传输的页面,可能在传输途中连 CSP 头一起被删除或改写,因此要用 HTTPS 确保策略能可靠送达的传输路径
  • X-Content-Type-Options: nosniff:防止 MIME 类型嗅探,阻止不应被解释为脚本的资源执行
  • CORS 的关系:两者作用的方向相反。CSP 处在限制本页面可以从哪些提供方加载资源的一侧,而 CORS 处在按照服务器的声明、仅在必要范围内打开同源策略默认关闭的响应读取的一侧

部署时费工夫的地方,是把现有的内联脚本和第三方标签逐一清点出来,改为附加 nonce 并整理提供方的写法。这项清点工作在运行年数越长的网站上量越大,因此在新项目中一开始就采用以 nonce 为前提的模板结构,之后就不必再补做针对 XSS点击劫持的对策。

常见误解

设置了 CSP 就不需要 XSS 防御措施了
CSP 是缓解 XSS 危害的纵深防御的一层,不能替代 XSS 的根本防御措施 (输出转义)。依赖对提供方列举 (白名单) 的策略,其绕过手法已在 2016 年的研究 (Lukas Weichselbaum 等「CSP Is Dead, Long Live CSP!」) 中被梳理,CSP Level 3 规范也把使用 nonce 或 hash 与 'strict-dynamic' 的构成列为推荐形式。同时实现输出转义和 CSP 是前提。
CSP 配置太难,小型站点不需要
即使是小型站点也存在 XSS 风险。从最小策略 (例如 default-src 'self'; script-src 'self') 开始,部署很容易。可以通过 Report-Only 模式确认影响,逐步收紧。
分享

相关术语

相关文章