沙箱插件默认是隔离的。要做超出读写自己的 KV 和存储之外的任何事情,插件必须在其描述符上声明一个 capability。沙箱桥基于这些声明控制每个主机提供的 API — 没有声明 content:read 的插件不会获得 ctx.content,没有声明 network:request 的插件不会获得 ctx.http。
本页介绍每个 capability 授予什么,沙箱如何强制执行它们,以及什么是不可强制执行的。
声明 capabilities
Capabilities 位于描述符(由 astro.config.mjs 导入的文件)上,与 id、version 和 entrypoint 一起:
export function helloPlugin(): PluginDescriptor {
return {
id: "plugin-hello",
version: "0.1.0",
format: "standard",
entrypoint: "@my-org/plugin-hello/sandbox",
capabilities: ["content:read", "network:request"],
allowedHosts: ["api.example.com"],
};
}
只声明插件实际需要的内容。Capability 声明也是市场向站点运营者在同意对话框中显示的内容 — 额外的 capabilities 在安装时会造成摩擦,在审计中是一个安全标记。
Capability 参考
| Capability | 授予访问权限 |
|---|---|
content:read | ctx.content.get(), ctx.content.list() |
content:write | ctx.content.create(), ctx.content.update(), ctx.content.delete() (隐含 content:read) |
media:read | ctx.media.get(), ctx.media.list() |
media:write | ctx.media.getUploadUrl(), ctx.media.upload(), ctx.media.delete() (隐含 media:read) |
network:request | ctx.http.fetch() — 限制为 allowedHosts |
network:request:unrestricted | ctx.http.fetch() 无主机限制(仅用于用户配置的 URL) |
users:read | ctx.users.get(), ctx.users.getByEmail(), ctx.users.list() |
email:send | ctx.email.send() (需要配置的电子邮件提供者插件) |
hooks.email-transport:register | 允许注册独占的 email:deliver 钩子(传输提供者) |
hooks.email-events:register | 允许注册 email:beforeSend / email:afterSend 钩子 |
hooks.page-fragments:register | 允许注册 page:fragments 钩子(仅原生插件) |
一些值得了解的事情:
- 隐含关系。
content:write自动隐含content:read;media:write隐含media:read;network:request:unrestricted隐含network:request。你不需要同时列出两者。 network:request:unrestricted存在于用户配置的 URL。 运营者输入目标 URL 的 webhook 插件需要访问清单中没有的主机。始终调用已知 API 的插件应该使用network:request+allowedHosts。email:send由配置控制,而不仅仅是 capability。 插件可以声明email:send,但只有在其他插件注册了email:deliver传输时,ctx.email才会被填充。
网络主机允许列表
具有 network:request 的插件只能获取 allowedHosts 中列出的主机。子域支持通配符:
capabilities: ["network:request"],
allowedHosts: [
"api.example.com", // 精确主机
"*.cdn.example.com", // cdn.example.com 的任何子域
],
桥在转发请求之前会根据允许列表检查请求 URL 的主机。对未声明主机的请求会在插件内部抛出,而不会离开沙箱。
network:request:unrestricted 完全跳过允许列表检查。它适用于运营者在运行时配置目标 URL 的插件(webhook 发送者、通用 HTTP 转发器)。避免在目标是插件设计一部分的插件中使用它 — 改为用明确的主机声明 network:request,以便同意对话框准确告诉运营者插件将调用哪里。
沙箱强制执行的内容
当沙箱运行器处于活动状态时,运行时强制执行:
-
Capability 门控。 PluginContext 工厂仅在声明相应 capability 时填充
ctx.content、ctx.media、ctx.http、ctx.users、ctx.email。无法在未声明的 capability 上调用方法 — 那里没有对象。 -
存储和 KV 作用域。 每个存储和 KV 操作都限定为插件的 id。插件无法读取另一个插件的 KV 或其存储集合,并且只能访问在描述符上声明的存储集合。
-
网络隔离。 直接
fetch()和其他网络原语被运行器阻止。访问网络的唯一方式是ctx.http.fetch(),它通过桥的主机验证。 -
无主机绑定。 沙箱插件看不到环境变量、文件系统或任何平台绑定 — 即使你的主机工作器有它们。插件运行时是一个干净的隔离,只有桥和声明的 capabilities。
-
资源限制。 运行器可以对每次调用强制执行 CPU、子请求、挂钟时间和内存限制。确切的限制取决于你使用的运行器;Cloudflare 运行器使用平台的 Worker Loader 限制(每次调用 50ms CPU、10 个子请求、30 秒挂钟时间、约 128MB 内存)。超出运行器限制的钩子会被中止;EmDash 钩子超时(钩子配置中的
timeout)在此基础上强制执行更严格的上限。
沙箱不强制执行的内容
capability 系统无法涵盖的一些事情:
- 授予 capability 内的行为。 具有
content:write的插件可以编辑任何内容,而不仅仅是它自己的内容。Capabilities 是粗粒度的 — 它们说”此插件可以写入内容”,而不是”此插件只能写入它创建的内容”。审计时审查是对插件在其授权内实际所做事情的唯一检查。 - Node.js 上的运营者信任。 当配置的沙箱运行器报告不可用时(无 Cloudflare Worker Loader、未安装 Node 端运行器等),
sandboxed: []插件在启动时被跳过。你可以将它们移至plugins: []以在进程中运行它们 — 但这样就没有 V8 隔离、没有资源限制,插件可以直接调用fetch()或读取环境变量。将其视为原生级别的信任。 - 侧信道。 时序、日志输出和存储数据对任何有适当访问主机环境权限的人都是可见的。不要将沙箱用作针对运行它的运营者的机密性边界。
Capability 同意
当运营者从市场安装沙箱插件时,EmDash 会显示一个同意对话框,列出声明的 capabilities。添加 capabilities 的更新 — 例如,以前只读取内容的插件现在想要发出网络请求 — 显示为 capability 差异,并且在新版本生效之前需要重新批准。
这就是为什么即使你”以后可能使用它们”,声明额外的 capabilities 也很重要。它们在每次安装和更新时都会显示为摩擦,安全审计会标记要求超出明显需要的插件。准确列出插件使用的内容,并在插件实际开始使用时在真实版本中添加新的 capabilities。
打包时验证
emdash plugin bundle 和 emdash plugin publish 执行额外的检查:
- 每个声明的 capability 必须在已知集合中(拼写错误会导致构建失败)。
- 在未填充
allowedHosts的情况下声明network:request的插件会触发警告 — 声明主机或切换到network:request:unrestricted并说明原因。 - 弃用的 capability 名称在
bundle/validate期间触发警告,在publish时硬失败。 - 打包的
backend.js无法导入 Node.js 内置模块(fs、path、child_process等) — 沙箱运行时不提供它们。
有关检查的完整列表,请参阅打包和发布。