CSP 与扩展安全
本教程共 56 篇 · 第 44 篇 · 更新于 2026-08-13 · 约 9 分钟阅读
本节目标:学完你能说清 Manifest V3 下扩展默认那条内容安全策略是什么、内联脚本和远程脚本为什么一律不行、你能把策略改到什么边界,以及 Chrome 这么设计到底在防什么。
第 11 章我们已经提过,扩展清单里有个 content_security_policy 字段。那章只讲了”它是什么、怎么写一条最小的”。这一节把这件事讲透:Chrome 给扩展默认装了一道什么样的护栏,哪些写法会直接被拦,你手里又能动多少。安全这东西,懂了原理才不会天天和报错较劲。
44-1 先弄明白 CSP 是什么
CSP 全称 Content Security Policy,中文叫”内容安全策略”。它是一套告诉浏览器”哪些来源的代码允许执行”的规则。对普通网页来说,CSP 是一层可选加固;但对 Chrome 扩展来说,它是强制底座——你写的扩展,从装上那一刻起就跑在一条严格策略之下。
它的核心思路特别朴素:把”能执行的代码”锁死在你能掌控的范围内。来源不在白名单里的脚本,浏览器直接拒绝执行,连报错都来不及。这样即便扩展被注入了恶意内容,攻击者也很难借机跑起自己的代码。
扩展的 CSP 写在清单的 content_security_policy 字段里。在 Manifest V3 下,这个字段是一个对象,必须有 extension_pages 这个键:
{
"manifest_version": 3,
"name": "My extension",
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self';"
}
}
extension_pages 管的是扩展自己的页面——popup、options、side panel,以及后台服务工作者(Service Worker)都算。注意后面那串字符串的写法:script-src 管脚本来源,object-src 管插件对象(比如 Flash 这类,虽然现在基本用不到)。后面跟着的 'self',意思是”只认扩展包内部的文件”。
Note内容脚本不归
extension_pages这条策略管。内容脚本运行在隔离环境里,它的行为受自身机制约束,和扩展页面是两套逻辑。后面章节还会细说,这里先知道”扩展页面”和”内容脚本”不是同一道锁”即可。
44-2 默认规则到底是什么
如果你在清单里压根不写 content_security_policy,Chrome 也会给你套上一条默认策略。按 Chrome 官方口径,MV3 扩展页面的默认(也是最小)策略是:
script-src 'self' 'wasm-unsafe-eval'; object-src 'self';
它比上一节示例里写的 script-src 'self'; object-src 'self'; 多了一个 'wasm-unsafe-eval'——这是 Chrome 默认给 WebAssembly 留的口子(见 44-6)。也就是说,显式写出策略并省略 'wasm-unsafe-eval',等于在默认基础上做了收紧。具体落到三件事上:
- 只能加载扩展自己打包进去的
.js文件。 - 不能执行写在 HTML 里的内联脚本。
- 不能从任何远程地址(CDN、你自己的服务器、外链)拉代码。
这三条是 Chrome 给你定的底线,不是建议你遵守,而是浏览器直接强制执行。你写不写这条字段,它都在。显式写出来,只是为了让意图更清楚,或者在默认基础上做”有限收紧”。
Tip我建议新手把这条策略显式写在清单里。一来别人读你的清单一眼就知道安全要求,二来一旦你确实需要放宽某一点(比如本地开发要连 localhost),你也清楚是从哪条基线改起的。
44-3 内联脚本为什么一律不行
内联脚本,指的是直接写在 HTML 里的代码。最典型的几种都在这个清单里:
- HTML 里直接写
<script>alert('hi')</script>这种标签块。 - 元素上的事件属性,比如
<button onclick="doSomething()">。 javascript:开头的链接,比如<a href="javascript:...">。
在扩展页面里,上面这些全都不会执行。浏览器看到内联代码,直接丢弃,控制台给你一条 CSP 报错。
这条限制常让刚上手的人懵:我明明写对了逻辑,为什么点了没反应?十有八九是手滑把 onclick 写在了标签上。正确做法是把所有 JS 抽到单独的 .js 文件,用 <script src="xxx.js"></script> 引入,然后在 JS 里用 addEventListener 绑事件:
<!-- 错误:内联事件会被 CSP 拦掉 -->
<button onclick="handleClick()">点我</button>
<!-- 正确:逻辑抽到外部文件 -->
<button id="myBtn">点我</button>
<script src="popup.js"></script>
// popup.js
document.getElementById("myBtn").addEventListener("click", () => {
console.log("按钮被点了");
});
Warning调试时若发现”脚本不执行、事件没反应”,先开控制台看有没有
Refused to execute inline script这类字样的报错。别急着怀疑别的,把内联代码搬到外部.js基本就能好。
44-4 远程脚本为什么被禁
和”内联”相对的另一个禁区,是”远程”。在扩展页面里,你不能这样引入脚本:
<!-- 错误:从远程地址加载 JS,违反 CSP 与远程代码禁令 -->
<script src="https://cdn.example.com/lib.js"></script>
也不能在代码里动态拉一段远程 JS 再执行。背后的道理很直接:一旦允许扩展在运行时从外部服务器拉代码,那商店审查时看到的代码,和用户实际跑的代码就不是同一份了。审查形同虚设,用户也失去了保障。
这条禁令和下一节要讲的”禁止远程托管代码”是同一件事的两面:CSP 从技术层面把你拦住,发布规范从流程层面把你卡住。双管齐下,目的就是让”扩展实际运行的代码”等于”提交审查的代码”。
44-5 eval 与 new Function 也默认封死
很多人习惯用 eval("...") 或 new Function("...") 动态执行字符串。在扩展页面里,这俩默认也是被封的。同理,setTimeout("字符串代码", 0) 这种把代码当字符串传的写法也不行。
原因和内联脚本一样:动态字符串执行,是注入攻击最爱借的通道。一旦你允许它,恶意输入就可能变成可执行的代码。
这里有个关键区别要记牢:Manifest V3 允许你往策略里加 'wasm-unsafe-eval',这是为了支持 WebAssembly 这类合法需求;但 'unsafe-eval' 在扩展里是绝对加不进去的,哪怕你显式写进 CSP 字符串,浏览器也不会认。换句话说,eval 这条路在扩展里是被彻底堵死的,不是”收紧”,而是”焊死”。
44-6 你能把策略改到什么边界
这是新手最容易误解的地方。你可以自定义 extension_pages 的策略,但方向只有一个:只能更严,或者加上 Chrome 明确许可的少数几项;绝不能放开到允许任意远程或任意 eval。
下面是几个合法、常见的改法。
允许 WebAssembly 执行(需要 'wasm-unsafe-eval'):
{
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
}
}
本地开发时临时放行 localhost(仅调试用,发布前通常不需要):
{
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:*; object-src 'self';"
}
}
如果你想对扩展页面再收紧一步,比如连 object-src 都锁死、只允许同源,就写成更明确的 default-src 'self':
{
"content_security_policy": {
"extension_pages": "default-src 'self'"
}
}
Tip上面这些写法里,
'self'之外的每一项都是”有限许可”:wasm 给 WebAssembly,localhost给本地调试。它们都不会让你的扩展能偷偷从远程拉代码。记住这个边界——凡是会放开”远程可执行代码”的写法,Chrome 一律不接受。
44-7 沙盒页面:实在要跑不可信代码怎么办
有些扩展确实需要在页面里执行一些不完全可信的代码,比如渲染用户提供的模板、跑第三方插件。直接放进扩展页面会被 CSP 管着,也不该让这些代码碰扩展 API。
Manifest V3 提供了沙盒机制。你在清单里声明 sandboxed_pages,列出哪些页面是沙盒页:
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self';",
"sandbox": "sandbox allow-scripts; script-src 'self';"
}
}
{
"sandboxed_pages": ["sandbox.html"]
}
沙盒页的特点要记清楚:它运行在一个被隔离、几乎拿不到扩展权限的环境里,不能直接调用 chrome.* API,也不能访问扩展的其他页面。这样即便里面的代码出问题,也伤害不到扩展主体和用户数据。它是一张”隔离实验台”,不是用来绕开 CSP 的后门。
44-8 Chrome 到底在防什么
把前面几条串起来,你会发现扩展的 CSP 设计逻辑非常一致:让”能执行的代码”来源单一、完全可见、不可在运行时被偷偷替换。具体防的是三类风险:
第一,防 XSS 注入。 内联脚本被内联事件被禁,攻击者就算往你的页面塞了 <img onerror="...">,那段代码也不会跑。把逻辑抽到外部文件,注入点就少了。
第二,防供应链投毒。 远程脚本被禁,意味着你依赖的库必须打包进扩展、跟着版本过审。某天你用的 CDN 被人挂马,扩展用户也不会中招,因为扩展根本不从那里取代码。
第三,防”审查漂移”。 这是最关键的。商店审查的是你提交的包,而 CSP 与远程代码禁令保证用户跑的也是这个包。没有运行时拉代码的口子,审查才有意义。
说实话,刚从网页开发转过来的人会觉得这套限制很”烦”:不能用 CDN、不能 eval、不能内联。但换个角度想,正是这些麻烦,让扩展这个能深度触碰浏览器和账号的实体,总体上比随便装的脚本安全得多。你写扩展时顺着它的设计走,反而省掉了大量安全兜底的工作。
44-9 给日常开发的几条提醒
- 所有 JS 都放外部文件,HTML 里只保留结构和
<script src>引入。 - 事件一律用
addEventListener,别碰onclick这类内联属性。 - 第三方库(jQuery、某个工具库)老老实实下载进扩展目录再引用,别图省事挂 CDN 链接。
- 真要本地联调开发服务器,用
'wasm-unsafe-eval'加localhost的写法,发布前把不需要的源去掉。 - 遇到脚本不执行,先查控制台 CSP 报错,八成是内联或远程被拦。
把这一节的规则吃透,你写出的扩展从根上就站在一个安全底座上。下一节我们接着讲”禁止远程托管代码”和发布时要过的那些安全关,那是 CSP 在发布流程里的延伸。