在 Chrome 扩展中运行 LLM 生成的代码:一场 MV3 CSP 冒险

发布于

Chrome Manifest V3 有一个至高无上的戒律:**你不得使用 `eval`**。不能使用 `new Function`,也不允许在扩展程序的特权上下文中使用动态代码字符串。这个政策让一代扩展程序遭受了重创,使得 MV3 的迁移异常痛苦。

现在考虑一下 [Scrapewright](https://github.com/singhand-labs/scrapewright) 的核心要求:它必须在运行时执行**由 LLM 编写的抓取代码**——任意生成的、安装后创建字符串形式的 JavaScript——在一个 Chrome 扩展中。这表面上看,正是 MV3 想要防止的事情。该仓库提供了这个难题的一个解决方案,它所采用的路径也揭示了 Chrome 实际的安全模型(与文档中描述的不同)。

## 代码运行的地图

一个 MV3 扩展大致有这些执行上下文,每个上下文都有其自己的 CSP:

| 上下文 | `eval`/`new Function`? | 权限 |
|-------------------------|-----------------------|--------------------|
| Service worker (`background.js`) | **否** | 完整的 `chrome.*` API |
| 扩展页面(弹出、选项) | **否** | 完整的 `chrome.*` API |
| 内容脚本(在页面中) | **否** | DOM 访问 + 有限的消息传递 |
| **声明的沙箱页面** | **是** | 无 — 隔离源 |

逃生通道就是最后一行:在 manifest 的 `sandbox` 关键字下列出的页面会在**具有唯一不透明来源的沙箱 iframe 中加载**——没有 `chrome.*` 的访问,没有与任何东西的同源关系,仅限于 DOM 访问。作为这种隔离的交换,Chrome 放宽了他们的 CSP,允许使用 `eval` 和 `new Function`。这是针对“我不信任的代码”的官方容器:插件、数学表达式求值器、REPL。

这是理论。实践就有趣了。

## 中转链

Scrapewright 生成的 LLM 步骤脚本不能仅仅停留在沙箱中进行计算——它们需要**接触到任意已打开标签页的 DOM**(点击、提取、悬停、滚动)。沙箱不能直接这样做(没有权限),而标签页自己的 CSP 禁止将求值器注入其中(许多网站也 CSP 阻止 `unsafe-eval`,将代码注入页面意味着必须生活在页面的威胁模型中)。

因此,一个请求中转链将两边连接起来:
```javascript
LLM 步骤脚本(沙箱 iframe)
│ "$extract('.item .price')"
▼ postMessage → DOM_REQUEST
离屏文档(offscreen.js — 管理沙箱 iframe)
▼ chrome.runtime 消息
服务工作者(background.js — 具有 chrome.tabs)
▼ chrome.tabs.sendMessage
目标标签页中的内容脚本(content-script.js)
▼ 执行 DOM 操作
└── 响应以相同方式回流 → 沙箱解析 Promise
```

值得借鉴的设计选择:
- **为何使用离屏文档?** MV3 服务工作者不能拥有 iframe;持有文档的上下文需要一个文档。`chrome.offscreen` 为扩展提供了一个隐藏页面,可以在 SW 暂停期间存活——这是沙箱 iframe 和映射并发 DOM 请求回其原始标签的 `tabIdStack` 的稳定宿主。
- **为何不将生成的代码注入目标页面?** 因为那样代码将在**页面的** CSP 和**页面的**更改表面下运行。在沙箱中,生成的代码就像一个囚犯,只能通过消息传递请求看守进行操作。LLM 的幻觉或恶意页面内容无法突破这一边界。
- **响应去重和超时。** 每个 `DOM_REQUEST` 带有一个 ID;在中转链中去重响应(内容脚本的回复可以同时直接到达和通过后台转播),超时路径則会拆解栈,以免悬挂的标签导致栈槽泄露。阅读这段代码是一堂防御性异步管道的课程。

## 哲学的部分

这个架构真正展示的是 MV3 的 CSP 不是对动态代码的禁止——而是一个**路由要求**:动态代码必须生活在一个没有权限的上下文中,以便能力只能通过显式、可审计的消息通道流动。沙箱囚犯从不处理不应有的能力;每一个特权行为(`chrome.tabs`、`chrome.debugger`、存储)都停留在审查时静态的代码中,并受到 manifest 所声明的权限的保护。

对于一个 LLM 驱动的系统,这不仅是卫生问题——而是 containment 故事。生成的代码**预期**是偶尔错误的(这就是中介)。在沙箱中的错误代码会导致 DOM 请求失败。而拥有 `chrome.debugger` 访问权限的错误代码则会导致不同层次的事件。该架构假设其生成器是易错的,而冲击半径便是关键。

## 去看看

这条链的规模小到你可以在一个下午记住——`sandbox.js`(求值器)、`offscreen.js` + `lib/offscreen-executor.js`(宿主/中转)、`background.js`(桥接)、`content-script.js`(页面中的求值器)——仓库的白皮书逐模块走过这些。如果你正在构建任何接触用户或模型创建代码的 MV3 项目,这是值得一小时阅读的先例:[github.com/singhand-labs/scrapewright](https://github.com/singhand-labs/scrapewright)(GPLv3,纯 JS,无构建步骤在你和代码之间)。

---

原文链接:[点击查看](https://www.v2ex.com/t/1236286)

评论

暂无评论。

0.042529s