沙箱插件默认是隔离的。要做超出读写自己的 KV 和 storage 之外的任何事情,插件必须在其 manifest 中声明一个 capability。沙箱桥基于这些声明控制每个主机提供的 API — 没有声明 content:read 的插件不会获得 ctx.content,没有声明 network:request 的插件不会获得 ctx.http。
本页介绍每个 capability 授予什么,沙箱如何强制执行它们,以及什么是不可强制执行的。
声明 capabilities
Capabilities 位于 emdash-plugin.jsonc 中,与 slug 和其余的信任契约一起:
{
"slug": "plugin-hello",
// ...identity + profile...
"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 hook(传输提供商) |
hooks.email-events:register | 允许注册 email:beforeSend / email:afterSend hooks |
hooks.page-fragments:register | 允许注册 page:fragments hook(仅限原生插件) |
一些值得了解的事情:
- 包含关系。
content:write自动包含content:read;media:write包含media:read;network:request:unrestricted包含network:request。你不需要同时列出两者。 network:request:unrestricted存在是为了用户配置的 URL。 一个 webhook 插件,其中运营者输入目标 URL,需要访问 manifest 中没有的主机。始终调用已知 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 上调用方法是不可能的 — 那里没有对象。 -
Storage 和 KV 作用域。 每个 storage 和 KV 操作都限定在插件的 slug 范围内。插件无法读取另一个插件的 KV 或其 storage 集合,它只能访问在 manifest 中声明的 storage 集合。
-
网络隔离。 运行器阻止直接的
fetch()和其他网络原语。到达网络的唯一方法是ctx.http.fetch(),它通过桥的主机验证。 -
无主机绑定。 沙箱插件看不到环境变量、文件系统或任何平台绑定 — 即使你的主机 worker 有它们。插件运行时是一个干净的隔离环境,只有桥和声明的 capabilities。
-
资源限制。 运行器可以对每次调用强制执行 CPU、子请求、挂钟时间和内存限制。确切的限制取决于你使用的运行器;Cloudflare 运行器使用平台的 Worker Loader 限制(每次调用 50ms CPU,10 个子请求,30 秒挂钟时间,~128MB 内存)。超过运行器限制的 hooks 将被中止;EmDash hook 超时(hook 配置中的
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 必须在认可的集合中(拼写错误会导致构建失败)。
network:request需要非空的allowedHosts;network:request:unrestricted需要它为空。参见 manifest 参考。- 打包的
backend.js不能导入 Node.js 内置模块(fs、path、child_process等)— 沙箱运行时不提供它们。
有关检查的完整列表,请参阅 Bundling and publishing。