插件沙箱

本页内容

EmDash 支持以两种执行模式运行插件:trustedsandboxed。本文说明各模式如何工作、提供哪些保护,以及不同部署目标下的安全含义。

执行模式

TrustedSandboxed
运行位置主进程隔离的 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,
				},
			],
		}),
	],
});

沙箱强制内容

  1. Capability 强制

    若插件声明 capabilities: ["read:content"],则只能调用 ctx.content.get()ctx.content.list()。尝试 ctx.content.create() 会抛出权限错误。由 RPC 桥接强制——插件无法绕过,因其没有直接数据库访问。

  2. 资源限制

    每次调用(钩子或路由)在以下限制下运行:

    资源默认值强制方
    CPU 时间50msWorker Loader(V8 isolate)
    子请求每次调用 10 次Worker Loader(V8 isolate)
    墙钟时间30 秒EmDash 运行器(Promise.race
    内存约 128MBV8 平台上限(不可按插件配置)

    超出 CPU 或子请求限制时,Worker Loader 会中止 isolate 并抛出异常。超出墙钟时间时,EmDash 会拒绝该次调用的 Promise。内存受 V8 上限约束,但无法按插件单独配置。

    以上为内置默认值。可通过自定义 SandboxRunnerFactory,经 SandboxOptions.limits 传入不同值。尚未支持通过 EmDash 集成配置按站点设置。

  3. 网络隔离

    Sandboxed 插件具有 globalOutbound: null,直接在 V8 层阻止 fetch()。必须使用经桥接代理的 ctx.http.fetch()。桥接会按插件的 allowedHosts 校验目标主机。

  4. 存储作用域

    所有存储操作(KV、collections)都限定在插件 ID 内;插件无法读取其他插件数据。内容与媒体访问经桥接,每次调用都会检查 capabilities。

  5. 功能限制

    部分功能仅在 Trusted 模式可用:

    • API routes — 无法使用自定义 REST 端点(routes)。Sandboxed 插件通过 Block Kit 管理页与钩子交互。
    • Portable Text 块类型 — PT 块需要用于站点侧渲染的 Astro 组件(componentsEntry),在构建时从 npm 加载。Sandboxed 插件在运行时安装,无法附带组件。
    • 自定义 React 管理页 — Sandboxed 插件使用 Block Kit 构建管理 UI,而非自行提供 React 组件。

    若插件声明了这些能力,emdash plugin bundle 会发出警告。

架构

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()
插件耗尽 CPUWorker Loader 中止 isolate无法阻止 — 阻塞事件循环
插件耗尽内存Worker Loader 终止 isolate无法阻止 — 可能导致进程崩溃
插件访问环境变量无访问(隔离 V8 上下文)无法阻止 — 共享 process.env
插件访问文件系统Workers 无文件系统无法阻止 — 完整 fs 访问

Node.js 部署建议

  1. 只从可信来源安装插件。 安装前审查源码,优先选择知名维护者的插件。
  2. 把 capability 当作审查清单。 即使不强制,也能记录预期范围。不需要网络却声明 ["network:fetch"] 很可疑。
  3. 监控资源使用。 使用进程级监控(例如 --max-old-space-size、健康检查)发现失控插件。
  4. 若需运行不可信插件,考虑 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 模式部署而无需改代码。