プラグインサンドボックス

このページ

EmDash ではプラグインを trustedsandboxed の 2 つの実行モードで動かせます。本ページでは各モードの動作、保護内容、デプロイ先ごとのセキュリティ上の意味を説明します。

実行モード

TrustedSandboxed
実行場所メインプロセス分離された V8 isolate(Dynamic Worker Loader)
Capabilities参考情報(強制されない)実行時に強制
リソース制限なしCPU、メモリ、サブリクエスト、経過時間
ネットワーク制限なしブロック。ctx.http とホスト許可リストのみ
データアクセスDB フルアクセス宣言した capabilities 経由の RPC ブリッジに限定
利用可能全プラットフォーム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 ランタイムを共有します。任意のモジュールの import、環境変数の参照、(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 ブリッジが強制するため、DB に直接触れないプラグインは迂回できません。

  2. リソース制限

    各呼び出し(フックまたはルート)は次の制限下で実行されます。

    リソースデフォルト強制主体
    CPU 時間50msWorker Loader(V8 isolate)
    サブリクエスト呼び出しあたり 10Worker Loader(V8 isolate)
    経過時間30 秒EmDash ランナー(Promise.race
    メモリ約 128MBV8 プラットフォーム上限(プラグイン単位では設定不可)

    CPU またはサブリクエスト超過で Worker Loader が isolate を中止し例外を投げます。経過時間超過で EmDash が呼び出し Promise を拒否します。メモリは V8 上限で抑えられますが、プラグインごとの設定はできません。

    これらは組み込みデフォルトです。SandboxRunnerFactorySandboxOptions.limits に別値を渡すとカスタム制限にできます。EmDash 統合設定によるサイト単位の設定は未実装です。

  3. ネットワーク分離

    Sandboxed プラグインは globalOutbound: null のため、直接の fetch() は V8 レベルでブロックされます。ctx.http.fetch() を使い、ブリッジ経由でプロキシされます。ブリッジは宛先ホストをプラグインの allowedHosts と照合します。

  4. ストレージのスコープ

    KV・コレクションなどのストレージ操作はプラグイン ID にスコープされます。他プラグインのデータは読めません。コンテンツ・メディアはブリッジ経由で、呼び出しごとに capability を検査します。

  5. 機能制限

    次は Trusted モードのみで利用可能です。

    • API routes — カスタム REST エンドポイント(routes)は使えません。Sandboxed プラグインは Block Kit の管理画面とフックでやり取りします。
    • Portable Text ブロック型 — PT ブロックはサイト側レンダリング用の Astro コンポーネント(componentsEntry)が必要で、ビルド時に npm から読み込まれます。Sandboxed プラグインは実行時インストールのためコンポーネントを同梱できません。
    • カスタム React 管理ページ — Sandboxed プラグインは React コンポーネントではなく Block Kit で管理 UI を構築します。

    これらを宣言していると 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 で動き、capability 検証のうえで実際の DB/ストレージ操作を行います。

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 なし防げない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 に載せられることが狙いです。