EmDash 支持以两种执行模式运行插件:trusted 与 sandboxed。本文说明各模式如何工作、提供哪些保护,以及不同部署目标下的安全含义。
执行模式
| Trusted | Sandboxed | |
|---|---|---|
| 运行位置 | 主进程 | 隔离的 V8 isolate(Dynamic Worker Loader) |
| Capabilities | 仅作说明(不强制) | 运行时强制 |
| 资源限制 | 无 | CPU、内存、子请求、墙钟时间 |
| 网络访问 | 无限制 | 被阻止;仅可通过 ctx.http 与主机白名单 |
| 数据访问 | 完整数据库访问 | 仅限通过 RPC 桥接访问已声明的 capabilities |
| 可用平台 | 所有平台 | 仅 Cloudflare Workers |
Trusted 模式
Trusted 插件与 Astro 站点在同一进程中运行,从 npm 包或本地文件加载,并在 astro.config.mjs 中配置:
import myPlugin from "@emdash-cms/plugin-analytics";
export default defineConfig({
integrations: [
emdash({
plugins: [myPlugin()],
}),
],
});
在 Trusted 模式下:
- Capabilities 是文档而非强制策略。 声明了
["read:content"]的插件仍可能访问进程内任意资源。capabilities字段向管理员说明插件 打算 使用什么。 - 无资源上限。 CPU、内存与网络使用不受限,异常插件可能拖住整个请求。
- 完整进程访问。 插件与 Astro 站点共享 Node.js/Workers 运行时,可导入任意模块、读取环境变量,并在 Node.js 上读写文件系统。
Sandboxed 模式(Cloudflare Workers)
Sandboxed 插件在 Cloudflare Dynamic Worker Loader API 提供的隔离 V8 isolate 中运行;每个插件拥有带强制限制的独立 isolate。
要启用沙箱,请在 Astro 配置中设置 sandbox runner:
export default defineConfig({
integrations: [
emdash({
sandboxRunner: "@emdash-cms/cloudflare/sandbox",
sandboxed: [
{
manifest: seoPluginManifest,
code: seoPluginCode,
},
],
}),
],
});
沙箱强制内容
-
Capability 强制
若插件声明
capabilities: ["read:content"],则只能调用ctx.content.get()与ctx.content.list()。尝试ctx.content.create()会抛出权限错误。由 RPC 桥接强制——插件无法绕过,因其没有直接数据库访问。 -
资源限制
每次调用(钩子或路由)在以下限制下运行:
资源 默认值 强制方 CPU 时间 50ms Worker Loader(V8 isolate) 子请求 每次调用 10 次 Worker Loader(V8 isolate) 墙钟时间 30 秒 EmDash 运行器( Promise.race)内存 约 128MB V8 平台上限(不可按插件配置) 超出 CPU 或子请求限制时,Worker Loader 会中止 isolate 并抛出异常。超出墙钟时间时,EmDash 会拒绝该次调用的 Promise。内存受 V8 上限约束,但无法按插件单独配置。
以上为内置默认值。可通过自定义
SandboxRunnerFactory,经SandboxOptions.limits传入不同值。尚未支持通过 EmDash 集成配置按站点设置。 -
网络隔离
Sandboxed 插件具有
globalOutbound: null,直接在 V8 层阻止fetch()。必须使用经桥接代理的ctx.http.fetch()。桥接会按插件的allowedHosts校验目标主机。 -
存储作用域
所有存储操作(KV、collections)都限定在插件 ID 内;插件无法读取其他插件数据。内容与媒体访问经桥接,每次调用都会检查 capabilities。
-
功能限制
部分功能仅在 Trusted 模式可用:
- API routes — 无法使用自定义 REST 端点(
routes)。Sandboxed 插件通过 Block Kit 管理页与钩子交互。 - Portable Text 块类型 — PT 块需要用于站点侧渲染的 Astro 组件(
componentsEntry),在构建时从 npm 加载。Sandboxed 插件在运行时安装,无法附带组件。 - 自定义 React 管理页 — Sandboxed 插件使用 Block Kit 构建管理 UI,而非自行提供 React 组件。
若插件声明了这些能力,
emdash plugin bundle会发出警告。 - API routes — 无法使用自定义 REST 端点(
架构
Sandboxed 插件通过 RPC 桥与 EmDash 通信:
┌─────────────────────┐ RPC ┌──────────────────────┐
│ Plugin Isolate │ ◄──────────► │ PluginBridge │
│ (Worker Loader) │ (binding) │ (WorkerEntrypoint) │
│ │ │ │
│ ctx.kv.get(k) │──────────────│► kvGet(k) │
│ ctx.content.list() │──────────────│► contentList() │
│ ctx.http.fetch(u) │──────────────│► httpFetch(u) │
└─────────────────────┘ └──────────────────────┘
│
▼
┌──────────────┐
│ D1 / R2 │
└──────────────┘
插件代码在 V8 isolate 中运行,收到的 ctx 上每个方法都是指向桥接的代理。桥接运行在主 EmDash worker 中,在验证 capabilities 后执行实际的数据库/存储操作。
Wrangler 配置
沙箱需要 Dynamic Worker Loader。在 wrangler.jsonc 中加入:
{
"worker_loaders": [{ "binding": "LOADER" }],
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "emdash-media" }],
"d1_databases": [{ "binding": "DB", "database_name": "emdash" }]
}
Node.js 部署
部署到 Node.js(或任何非 Cloudflare 平台)时:
- 使用
NoopSandboxRunner,返回isAvailable() === false。 - 尝试加载 Sandboxed 插件会抛出
SandboxNotAvailableError。 - 所有插件必须在
plugins数组中注册为 Trusted。 - Capability 声明仅供参考,不会被强制。
对安全意味着什么
| 威胁 | Cloudflare(Sandboxed) | Node.js(仅 Trusted) |
|---|---|---|
| 插件读取不应访问的数据 | 桥接 capability 检查阻止 | 无法阻止 — 插件拥有完整 DB 访问 |
| 插件发起未授权网络调用 | globalOutbound: null + 主机白名单阻止 | 无法阻止 — 可直接 fetch() |
| 插件耗尽 CPU | Worker Loader 中止 isolate | 无法阻止 — 阻塞事件循环 |
| 插件耗尽内存 | Worker Loader 终止 isolate | 无法阻止 — 可能导致进程崩溃 |
| 插件访问环境变量 | 无访问(隔离 V8 上下文) | 无法阻止 — 共享 process.env |
| 插件访问文件系统 | Workers 无文件系统 | 无法阻止 — 完整 fs 访问 |
Node.js 部署建议
- 只从可信来源安装插件。 安装前审查源码,优先选择知名维护者的插件。
- 把 capability 当作审查清单。 即使不强制,也能记录预期范围。不需要网络却声明
["network:fetch"]很可疑。 - 监控资源使用。 使用进程级监控(例如
--max-old-space-size、健康检查)发现失控插件。 - 若需运行不可信插件,考虑 Cloudflare。 若插件来源不明(如市场),请部署到提供沙箱的 Cloudflare Workers。
相同 API,不同保证
无论执行模式如何,插件代码相同。definePlugin() API、ctx 形态、hooks、routes、storage 用法一致;变化的是 是否强制:
// This plugin works in both trusted and sandboxed mode
export default definePlugin({
id: "analytics",
version: "1.0.0",
capabilities: ["read:content", "network:fetch"],
allowedHosts: ["api.analytics.example.com"],
hooks: {
"content:afterSave": async (event, ctx) => {
// In trusted mode: ctx.http is always present (capabilities not enforced)
// In sandboxed mode: ctx.http is present because "network:fetch" is declared
await ctx.http.fetch("https://api.analytics.example.com/track", {
method: "POST",
body: JSON.stringify({ contentId: event.content.id }),
});
},
},
});
目标是让作者在本地以 Trusted 模式快速迭代、便于调试,在生产以 Sandboxed 模式部署而无需改代码。