外掛沙箱

本頁內容

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. 資源限制

    每次呼叫(hook 或路由)在以下限制下執行:

    資源預設值強制方
    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 管理頁與 hook 互動。
    • 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 模式部署而無需改程式碼。