大多数开发者都知道应该为他们的应用添加内容安全策略(CSP)头部,但很少有人拥有真正起作用的策略。要么它过于宽松,从而失去了意义,要么它会破坏网站的一半内容,导致开发者在沮丧中将其移除。
本指南将介绍 CSP 的作用、哪些指令至关重要,以及如何编写一个能够阻止真实攻击且不破坏应用的头部。
CSP 实际上做了什么
内容安全策略告诉浏览器可以从哪里加载资源。仅从该域名加载脚本,从该 CDN 加载样式,从任何地方加载图片,不支持内联脚本。
当浏览器接收到 CSP 头部时,它会在执行任何内容之前强制执行这些规则。如果跨站脚本(XSS)攻击将恶意脚本注入到你的 HTML 中,CSP 会在浏览器层面阻止它——即使该脚本已经绕过了你的输入清理。
这就是核心价值:当输入验证失败时,CSP 是你的第二道防线。
关键指令
CSP 有数十个指令,但大多数生产环境的头部只需要六个:
| 指令 | 限制的内容 | 常见错误 |
|---|---|---|
default-src |
任何未明确列出的资源类型的默认值 | 环境 'self',然后忘记字体和框架未被覆盖 |
script-src |
JavaScript 源 URL 和内联执行 | 添加 'unsafe-inline' 以静音控制台错误 |
style-src |
CSS 源 URL 和内联样式块 | 忘记你的 CDN 或由 JS 库注入的内联样式 |
img-src |
图片源,包括数据 URI | 缺少 data: 用于 base64 图片, blob: 用于画布导出 |
connect-src |
XHR、fetch、WebSocket 和 EventSource 的目标地址 | 忘记分析端点和 API 子域名 |
frame-ancestors |
哪些来源可以将你的网站嵌入一个 <iframe> |
完全跳过——导致点击劫持完全暴露 |
frame-ancestors 取代了旧的 X-Frame-Options 头部,即使在你尚未完全覆盖其他 CSP 场景之前也值得添加。
为什么 unsafe-inline 破坏其作用
当你添加 'unsafe-inline' 到 script-src时,你告诉浏览器:执行你找到的任何脚本标签,无论它来自哪里。这包括 XSS 攻击注入的脚本。
'unsafe-eval' 更糟糕的是,它允许 eval(), Function(),并且 setTimeout("string"),即使在其他代码库中也常见,是攻击的常见向量。
包含这些文档的策略不会提供任何有意义的注入保护。攻击者并不关心你的合法脚本从哪里加载,只要他们能注入自己的内联脚本即可。
正确的方法:使用非ces 和哈希值
如果你有合法的内联脚本,有两个选项可以不牺牲你的策略:
非ces 为每个页面加载生成一个唯一令牌。服务器将其添加到 CSP 头部和内联脚本的 nonce 属性中。只有具有匹配非ces 的脚本才能执行。
<!-- CSP header -->
Content-Security-Policy: script-src 'nonce-abc123xyz'
<!-- Inline script with matching nonce -->
<script nonce="abc123xyz">
window.analyticsId = '...';
</script>
哈希值 计算特定脚本内容的 SHA-256 指纹。将该哈希值添加到 script-src 中,浏览器仅运行该特定脚本——而不会运行其他任何脚本。
Content-Security-Policy: script-src 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc='
非ces 适用于内容随请求动态变化的页面。哈希值适用于从不随部署更改的静态脚本。
首先以报告仅模式部署
在没有测试的情况下将 CSP 添加到现有网站,是导致生产环境崩溃的常见做法。请先使用报告仅头部:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-{random}'; report-uri /csp-violations
浏览器会应用规则并报告违规情况,但不会阻止任何内容。违规日志会告诉你需要在切换到执行模式之前添加哪些内容到策略中。
大多数导致网站崩溃的 CSP 部署都跳过了这一步。
一个典型的 SaaS 应用的真实 CSP
以下是一个使用 CDN、Google Analytics 和 Stripe 的生产级头部示例,每个指令都已注释:
Content-Security-Policy:
# Tight default: only load from your own origin
default-src 'self';
# Scripts: your origin, GA tag manager via nonce, Stripe.js
script-src 'self' https://www.googletagmanager.com https://js.stripe.com 'nonce-{SERVER_NONCE}';
# Styles: your origin and Google Fonts CSS
style-src 'self' https://fonts.googleapis.com;
# Fonts: your origin and Google Fonts CDN
font-src 'self' https://fonts.gstatic.com;
# Images: your origin, CDN subdomain, and base64 data URIs
img-src 'self' https://cdn.yourdomain.com data:;
# Fetch/XHR: your API, GA collection endpoint, Stripe API
connect-src 'self' https://www.google-analytics.com https://api.stripe.com;
# Stripe renders its fields in an iframe
frame-src https://js.stripe.com;
# Nobody should be framing your app
frame-ancestors 'none';
# Block <object> and <embed> entirely
object-src 'none';
# Force HTTP requests to upgrade to HTTPS
upgrade-insecure-requests;
将 {SERVER_NONCE} 带有每次请求生成的加密随机值——例如, base64_encode(random_bytes(16)) 在 PHP 中或 crypto.randomBytes(16).toString('base64') 在 Node 中。
常见的 CSP 错误
过于宽泛的通配符。 script-src * 允许来自任何来源的脚本。你可能根本就没有脚本策略。
忘记子域名。 'self' 仅覆盖你的确切来源。 https://api.yourdomain.com 是不同的来源,需要在 connect-src.
下显式列出 frame-ancestors. 点击劫持保护独立于 XSS 保护。请将其单独添加到其他指令集之外。
在多个地方设置 CSP。 如果 CDN 和你的应用都设置了 CSP 头部,行为会变得不可预测。请在单一层级设置它——通常是在你的源服务器或反向代理中。
使用 report-uri 没有处理程序。 违规会生成 POST 请求发送到你的端点,每次受影响的页面加载都会发生。要么处理这些违规,要么将其指向 Report URI 等服务。
无需猜测地构建你的头部
跟踪页面加载资源的每个域名——包括脚本、样式、字体、图片和 API 调用——会变得非常繁琐。一个在线的 CSP 头部生成器 让你可以直观配置每个指令,并输出一个可以直接粘贴到服务器配置中的生产级头部。
将其作为起点,然后在开发者工具中审核实际的网络请求,以发现生成器可能遗漏的内容。
过于宽松的 CSP 只是装饰,过于严格的 CSP 会破坏用户。从报告仅模式开始,逐步收紧,必要时使用非ces 来处理内联脚本。你的策略应让攻击者的工作更难,而不是让你的工作更难。
