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 橋接強制——外掛無法繞過,因其沒有直接資料庫存取。 -
資源限制
每次呼叫(hook 或路由)在以下限制下執行:
資源 預設值 強制方 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 管理頁與 hook 互動。 - 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 模式部署而無需改程式碼。