能力与安全

本页内容

沙箱插件默认是隔离的。要执行超出读写自身 KV 和存储之外的任何操作,插件必须在其清单声明能力。沙箱桥根据这些声明控制每个宿主提供的 API —— 未声明 content:read 的插件无法获得 ctx.content,未声明 network:request 的插件无法获得 ctx.http

本页介绍每个能力授予什么权限、沙箱如何强制执行它们,以及哪些是无法强制执行的。

声明能力

能力位于 emdash-plugin.jsonc 中,与 slug 和其余信任契约一起:

{
	"slug": "plugin-hello",
	// ...身份 + 配置...

	"capabilities": ["content:read", "network:request"],
	"allowedHosts": ["api.example.com"]
}

只声明插件实际需要的能力。能力声明也是 Marketplace 在同意对话框中向运营者展示的内容 —— 额外的能力是安装时的摩擦和审计时的安全信号。

能力参考

能力授予访问权限
content:readctx.content.get()ctx.content.list()
content:writectx.content.create()ctx.content.update()ctx.content.delete()(隐含 content:read
taxonomies:readctx.taxonomies.getAll()ctx.taxonomies.getTerms()ctx.taxonomies.getEntryTerms()
media:readctx.media.get()ctx.media.list()
media:writectx.media.getUploadUrl()ctx.media.upload()ctx.media.delete()(隐含 media:read
network:requestctx.http.fetch() —— 限制为 allowedHosts
network:request:unrestrictedctx.http.fetch() 无主机限制(仅用于用户配置的 URL)
users:readctx.users.get()ctx.users.getByEmail()ctx.users.list()
email:sendctx.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:readmedia:write 隐含 media:readnetwork:request:unrestricted 隐含 network:request。你不需要同时列出两者。
  • 分类法是一个独立的只读表面。 taxonomies:read 通过 ctx.taxonomies 授予对分类法定义、其术语以及分配给条目的术语的访问权限。它独立于 content:read —— 如果插件读取内容及其分类,请同时声明两者。插件没有对分类法的写入权限。
  • network:request:unrestricted 用于用户配置的 URL。 运营者输入目标 URL 的 Webhook 插件需要访问不在清单中的主机。始终调用已知 API 的插件应使用 network:request + allowedHosts
  • email:send 由配置控制,不仅仅是能力。 插件可以声明 email:send,但 ctx.email 只有在另一个插件注册了 email:deliver 传输时才会被填充。

网络主机允许列表

具有 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,以便同意对话框准确告知运营者插件将调用哪里。

沙箱强制执行什么

当沙箱运行器处于活动状态时,运行时强制执行:

  1. 能力门控。 PluginContext 工厂仅在声明了相应能力时才填充 ctx.contentctx.taxonomiesctx.mediactx.httpctx.usersctx.email。调用未声明能力上的方法是不可能的 —— 那里没有对象。

  2. 存储和 KV 作用域。 每个存储和 KV 操作都限定在插件的 slug 范围内。插件无法读取另一个插件的 KV 或存储集合,只能访问在清单中声明的存储集合。

  3. 网络隔离。 直接的 fetch() 和其他网络原语被运行器阻止。通往网络的唯一路径是 ctx.http.fetch(),它经过桥的主机验证。

  4. 无宿主绑定。 沙箱插件看不到环境变量、文件系统或平台绑定 —— 即使你的宿主 Worker 拥有它们。插件运行时是一个干净的隔离体,只有桥和声明的能力。

  5. 资源限制。 运行器可以对每次调用强制执行 CPU、子请求、挂钟时间和内存限制。确切限制取决于你使用的运行器;Cloudflare 运行器使用平台的 Worker Loader 限制(每次调用 50ms CPU、10 个子请求、30 秒挂钟时间、约 128MB 内存)。Node.js workerd 运行器(@emdash-cms/sandbox-workerd)通过 Promise.race 强制挂钟时间;CPU 和内存限制是 Cloudflare 平台功能,独立的 workerd 不强制执行。超过运行器限制的钩子会被取消;EmDash 钩子超时(钩子配置中的 timeout)额外强制执行更严格的上限。

沙箱不强制执行什么

能力系统不涵盖且无法涵盖的一些事项:

  • 授予能力范围内的行为。 具有 content:write 的插件可以编辑任何内容,而不仅仅是自己的。能力是粗粒度的 —— 它们说”这个插件可以写入内容”,而不是”这个插件只能写入它创建的内容”。审计时的审查是对插件在其授权范围内实际行为的唯一检查。
  • Node.js 上的运营者信任。 如果配置的沙箱运行器报告不可用(没有 Cloudflare Worker Loader,没有安装 Node 端运行器等),sandboxed: [] 插件在启动时会被跳过。你可以将它们移动到 plugins: [] 以在进程内运行 —— 但那样就没有 V8 隔离体、没有资源限制,插件可以直接调用 fetch() 或读取环境变量。将此视为原生级别的信任。
  • 侧信道。 时序、日志输出和存储的数据对任何合理访问宿主环境的人都是可见的。不要将沙箱用作针对运行它的运营者的机密性边界。

能力同意

当运营者从 Marketplace 安装沙箱插件时,EmDash 会显示一个包含声明能力的同意对话框。添加能力的更新 —— 例如,一个之前只读取内容的插件现在想要发起网络请求 —— 会显示为能力差异,并在新版本生效前需要重新批准。

这就是为什么声明额外的能力很重要,即使你”以后可能需要它们”。它们在每次安装和更新时都会显示为摩擦,安全审计会标记要求超出明显需要的插件。准确列出插件使用的内容,并在插件实际开始使用时在真正的版本中添加新能力。

构建时验证

emdash-plugin bundleemdash-plugin publish 执行额外检查:

  • 每个声明的能力必须在已识别的集合中(拼写错误会导致构建失败)。
  • network:request 需要非空的 allowedHostsnetwork:request:unrestricted 需要它为空。参见清单参考
  • 打包的 backend.js 不能导入 Node.js 内置模块(fspathchild_process 等)—— 沙箱运行时不提供它们。

完整检查列表请参见打包和发布