Published on

CSP(内容安全策略)

在现代 Web 安全的纵深防御体系中,内容安全策略 (Content Security Policy, CSP) 是一道至关重要的防线。它是一种由 W3C 标准化的计算机安全机制,其核心目标是检测并缓解特定类型的攻击,尤其是跨站脚本攻击 (XSS) 和数据注入攻击。CSP 赋予了 Web 应用开发者一套精细的控制权,允许其明确地定义一个“白名单”,告诉浏览器只允许加载和执行来自可信来源的资源。

CSP 的部署与工作原理

CSP 通过服务器发送一个 Content-Security-Policy HTTP 响应头来部署。浏览器接收到此头部后,会严格遵循其中定义的策略来加载和执行页面资源。

CSP 的部署方式

  • HTTP 响应头 (推荐): Content-Security-Policy: <policy-string>,这是最常用、最安全的部署方式。
  • <meta> 标签: <meta http-equiv="Content-Security-Policy" content="<policy-string>">,可以作为备用,但功能受限(例如,无法使用 frame-ancestorsreport-uri 等指令)。

其核心机制是基于“指令 (directives)”的白名单策略。开发者通过组合不同的指令,来为不同类型的资源(脚本、样式、图片等)设定允许加载的来源列表。

核心指令 (Directives) 详解

CSP 策略由一个或多个指令组成,指令之间用分号 ; 分隔。

一个中等强度的 CSP 策略

Content-Security-Policy: default-src 'self'; script-src 'self' https://apis.google.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';
  • default-src 'self': 默认只信任同源资源。'self' 是一个特殊的关键字。
  • script-src 'self' https://apis.google.com: 脚本只能从同源或 https://apis.google.com 加载。
  • style-src 'self' 'unsafe-inline': 样式表只能从同源加载,但允许内联样式(<style> 标签和 style 属性),这在某些情况下是必要的,但会降低安全性。
  • img-src 'self' data:: 图片只能从同源或以 data: URL 的形式加载。
  • frame-ancestors 'none': 禁止页面被嵌入到任何 iframe 中。

常用资源指令

  • default-src: 这是一个备用指令,用于为大多数其他资源指令(如 script-src, style-src 等)提供默认值。
  • script-src: 定义允许加载和执行 JavaScript 的有效来源。
  • style-src: 定义允许加载和应用样式的有效来源(CSS 文件、<style> 标签等)。
  • img-src: 定义允许加载图片的有效来源。
  • font-src: 定义允许加载字体的有效来源。
  • connect-src: 限制通过脚本接口(如 fetch, XMLHttpRequest, WebSocket)可以连接的 URL。
  • media-src: 定义允许加载 HTML5 <audio><video> 元素的有效来源。
  • object-src: 定义允许加载 <object>, <embed><applet> 标签中插件的有效来源。出于安全考虑,通常建议设置为 'none'
  • child-src: 定义允许通过子上下文(如 <iframe>, <worker> 等)加载的资源来源。
  • frame-src: CSP Level 3 引入,用于更细粒度地控制 <frame><iframe> 的来源。如果同时指定 child-srcframe-src,那么 <iframe> 遵循 frame-src,其他子上下文遵循 child-src

文档指令

  • frame-ancestors: 指定哪些源可以通过 <frame>, <iframe>, <object> 等标签嵌入当前页面。这是防御点击劫持 (Clickjacking) 的最有效手段。
    • frame-ancestors 'self': 只允许被同源页面嵌入。
    • frame-ancestors 'none': 完全禁止页面被任何来源嵌入。

报告指令

  • report-uri / report-to: 指定当 CSP 策略被违反时,浏览器将违规报告(一个 JSON 对象)发送到的 URL。report-uri 是一个较早的指令,而 report-to 是基于新的 Reporting API 的现代化指令。

应对内联脚本与样式

默认情况下,一个严格的 CSP 会禁止所有内联脚本和样式(例如 <script>...</script>onclick="..."),因为这是 XSS 攻击最常见的向量。使用 'unsafe-inline' 关键字可以重新启用它们,但这会削弱 CSP 的安全屏障。现代 CSP 提供了两种更安全的技术来允许特定的内联脚本。

使用 nonce (Number used once)

服务器为每一次页面请求生成一个唯一的、不可预测的随机令牌(通常是 Base64 编码的字符串)。然后,将这个令牌同时放入 CSP 头的 script-srcstyle-src 指令中,并作为 nonce 属性添加到 HTML 的 <script><style> 标签上。浏览器只会执行 nonce 属性值与 CSP 头中声明的令牌相匹配的内联脚本。

Nonce-based CSP 实现

服务器响应头:

Content-Security-Policy: script-src 'nonce-aBcDeFg12345'

HTML 页面:

<script nonce="aBcDeFg12345">console.log("This script will execute.");</script>
<script nonce="aBcDeFg1234567">alert("This script will be blocked by CSP.");</script>

这个 nonce 值必须在每次页面加载时都重新生成,不可复用。

使用 hash (哈希值)

对于静态的、内容不变的内联脚本,可以预先计算出其内容的 SHA256、SHA384 或 SHA512 哈希值。然后,将这个哈希值(Base64 编码后)放入 CSP 头的 script-srcstyle-src 指令中。浏览器在解析页面时,会计算它遇到的每一个内联脚本的哈希值,并与头部中提供的哈希白名单进行比对。

Nonce vs. Hash

  • Nonce: 适用于动态生成的内联脚本,因为服务器可以在生成脚本的同时生成 nonce。
  • Hash: 适用于静态的、在构建时内容就已确定的内联脚本。

部署策略

直接部署一个严格的 CSP 可能会破坏现有应用的功能。因此,推荐采用循序渐进的策略:

  1. 使用 Content-Security-Policy-Report-Only: 在此模式下,浏览器仅报告违规行为,而不实际阻止任何资源的加载。这允许你在不影响用户的情况下,收集违规报告并逐步完善你的策略。
  2. 分析报告: 通过 report-urireport-to 收集违规报告,识别出所有必须被加入白名单的资源来源。
  3. 切换为强制模式: 当违规报告不再出现时,将 Content-Security-Policy-Report-Only 头替换为 Content-Security-Policy 头,正式启用强制执行策略。