上篇我们聊了同源策略——浏览器的国境线。它解决的是"谁能读我的数据"。但有一个问题它管不了:如果恶意代码已经混进了你的页面呢?
一段注入的 <script>,跟你的业务代码同源,同源策略不会拦它。它拿 Cookie、改 DOM、往外发请求,畅通无阻。
同源策略管的是"别人能不能进来看",CSP 管的是"进来之后能做什么"。
这就是今天要聊的 Content Security Policy(内容安全策略)。它不是同源策略的替代品,而是补上了同源策略的盲区——从"访问隔离"升级到"执行权限控制" 。
一、先搞清楚一个关键区别
同源策略和 CSP 经常被混为一谈,但它们的分工完全不同:
| 同源策略(SOP) | CSP | |
|---|---|---|
| 谁定的规矩 | 浏览器内置,你改不了 | 你自己配,主动声明 |
| 控制方向 | 管数据"流出"——阻止跨源读取 | 管资源"流入"——限制页面能加载什么 |
| 核心问题 | "别人能不能偷看我的数据?" | "我的页面能执行谁的代码?" |
| 类比 | 国境线上的护照检查 | 城内的营业执照制度 |
同源策略像皮肤,是天生的被动屏障;CSP 像白细胞,是你主动部署的执行层。两层加一起,才是完整的免疫系统。
一个管"谁能进来",一个管"进来的人能干什么"。
二、CSP 到底在防什么
一个字:XSS。
跨站脚本攻击(XSS)至今仍是 OWASP Top 10 里的常客。攻击者把恶意代码注入你的页面,浏览器分不清这是你写的还是攻击者写的,照单全收。
注入的姿势有很多种:
| 注入形式 | 示例 |
|---|---|
| 恶意外部脚本 | <script src="https://evil.com/hack.js"> |
| 内联脚本 | <script>偷Cookie</script> |
| 事件处理器 | <img onmouseover="偷Cookie"> |
| javascript: URL | <a href="javascript:偷Cookie"> |
| 危险 API | eval("偷Cookie") |
CSP 的思路很直接:既然我分不清好代码和坏代码,那我就只允许"持证上岗"的代码执行。
没有执照?不管你是谁,一律拦截。
三、执照怎么发
CSP 通过 HTTP 响应头传递给浏览器:
Content-Security-Policy: script-src 'self'; object-src 'none'
这句话翻译成人话:只允许加载同源的脚本,禁止所有插件。
核心指令就这么几个:
| 指令 | 管什么 |
|---|---|
default-src | 兜底策略,没单独配的都用它 |
script-src | JavaScript——最关键的一条 |
style-src | CSS 样式表 |
img-src | 图片 |
connect-src | XHR / Fetch / WebSocket |
frame-ancestors | 谁能用 iframe 嵌入你(防点击劫持) |
object-src | Flash 等插件(建议直接 'none') |
base-uri | <base> 标签(建议直接 'none') |
指令后面跟的值叫"源表达式",常用的有:
• 'self' — 只允许同源
• 'none' — 完全禁止
• https://cdn.example.com — 指定域名
• 'nonce-随机值' — 持有对应令牌的脚本
• 'sha256-哈希值' — 内容匹配的脚本
CSP 的设计哲学是"默认拒绝,显式放行"——跟防火墙的白名单规则一模一样。
四、白名单为什么靠不住
直觉上,列一份域名白名单似乎就够了:
Content-Security-Policy: script-src trusted-cdn.com analytics.com
但 2016 年 Google 安全团队的一篇论文(CSP Is Dead, Long Live CSP! )给了行业一记重锤。他们扫描了超过 10 亿个主机名,分析了 26,011 种唯一的 CSP 策略,结论是:
| 指标 | 数据 |
|---|---|
| 白名单 CSP 可被绕过的比例 | 75.81% |
| 对 XSS 防御完全无效的策略 | 99.34% |
| 前 15 个最常被白名单引用的域中不安全的 | 14/15 个 |
为什么?因为白名单域上往往存在 JSONP 接口、Angular 模板引擎、可控回调参数这些"合法但可被利用"的入口。攻击者不需要绕过你的 CSP,只需要找到你白名单里那个域的一个 JSONP 接口:
<script src="trusted-cdn.com/jsonp?callback=alert(1)//"></script>
CSP 看到 trusted-cdn.com——放行。攻击完成。
这就像城市的安保只看营业执照上的公司名,不看员工具体在干什么。名单再长,也防不住内部人作恶。
白名单管的是"你从哪来",但攻击者早就学会了借壳。
五、Strict CSP:从查身份到查指纹
Google 那篇论文的解决方案是 Strict CSP——不再信任域名,改为信任具体的代码。
两种方式:
Nonce(一次性令牌)
服务端每次响应生成一个随机值,同时放进 CSP 头和 <script> 标签:
Content-Security-Policy: script-src 'nonce-a1b2c3d4'
<script nonce="a1b2c3d4" src="/main.js"></script>
<script nonce="a1b2c3d4">console.log("合法")</script>
浏览器对比两边的 nonce,匹配才执行。攻击者注入的脚本没有 nonce,直接被拦。
关键要求:每次请求生成新的 nonce,不可预测,不可复用。
Hash(内容指纹)
对脚本内容算 SHA-256,把哈希值写进 CSP 头:
Content-Security-Policy: script-src 'sha256-abc123...'
浏览器收到脚本后重新算哈希,匹配才执行。内容被篡改一个字符,哈希就对不上。
| Nonce | Hash | |
|---|---|---|
| 适合场景 | 动态渲染(SSR / 模板引擎) | 静态页面 / 客户端渲染 |
| 每次请求变化 | ✅ 是 | ❌ 否 |
| 脚本改了要更新吗 | 不用 | 必须重新算哈希 |
Nonce 像每次进门换一把新钥匙,Hash 像给每个包裹贴了防拆封条。两者的共同点是:不再信任"你从哪来",只信任"你是不是那个人"。
这跟安全领域的零信任(Zero Trust)架构是同一个思路——永远验证,绝不默认信任。
六、strict-dynamic:信任链的传递
现实中你会遇到一个问题:你信任的主脚本 main.js 可能会动态加载其他脚本,这些子脚本没有 nonce,会被 CSP 拦住。
strict-dynamic 解决这个问题:被 nonce/hash 信任的脚本,它加载的子脚本也自动被信任。
Content-Security-Policy: script-src 'nonce-a1b2c3d4' 'strict-dynamic'
// main.js(有 nonce,被信任)
const s = document.createElement("script");
s.src = "analytics.js";
document.head.appendChild(s);
// analytics.js 也被允许执行
这像公司的信任传递:CEO 信任 VP,VP 招进来的人也获得一定权限。但风险也在这里——如果被信任的脚本本身有漏洞,信任链就被污染了。
所以 strict-dynamic 是一个务实的折中:用可控的信任范围,换取第三方脚本的兼容性。
七、推荐的 Strict CSP 模板
MDN 和 Google 推荐的生产环境配置:
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
upgrade-insecure-requests;
report-to csp-endpoint
拆解一下:
| 指令 | 作用 |
|---|---|
script-src 'nonce-...' 'strict-dynamic' | 只执行持令牌的脚本,信任链可传递 |
object-src 'none' | 彻底封死 Flash 等插件入口 |
base-uri 'none' | 防止 <base> 标签被利用做路径劫持 |
frame-ancestors 'none' | 禁止被 iframe 嵌入(防点击劫持) |
upgrade-insecure-requests | 自动把 HTTP 升级为 HTTPS |
report-to | 违规行为上报到你的监控端点 |
先用 Report-Only 模式测试,确认不影响功能,再切到强制模式。 这是部署 CSP 最重要的实操原则——先观察,再执行,别一上来就把自己的网站搞挂了。
八、一个架构原则
CSP 给前端架构带来了一个深层约束:第三方脚本的接入,必须经过策略层审批,不能直接穿透到业务域。
没有 CSP 的时代,产品经理说"加个统计代码",开发就往页面里塞一个 <script>。这个脚本能做什么、会加载什么、数据发到哪里,没人知道。
有了 CSP,每个外部脚本都必须通过你的 nonce 或 hash 审批。这不是增加了负担——这是把"谁能在我的地盘执行代码"这个问题,从隐性变成了显性。
好的架构从来不是没有约束,而是约束放在了正确的位置。
CSP 不是限制,是治理。同源策略画了国境线,CSP 建了城内的执法体系。
如果你只想带走一句话,我建议记这个:
同源策略管"谁能进来",CSP 管"进来之后能做什么"。安全不是一堵墙,是一套分层的权限体系。
参考原文:
• MDN Web Docs — Content Security Policy (CSP)
• Google Security Team — CSP Is Dead, Long Live CSP! On the Insecurity of Whitelists and the Future of Content Security Policy(2016)