Plugin-Sandbox

Auf dieser Seite

EmDash kann Plugins in zwei Ausführungsmodi betreiben: trusted und sandboxed. Diese Seite erklärt, wie die Modi funktionieren, welche Schutzmaßnahmen sie bieten und welche Sicherheitsimplikationen für verschiedene Deployment-Ziele gelten.

Ausführungsmodi

TrustedSandboxed
Läuft inHauptprozessIsolierte V8-Isolate (Dynamic Worker Loader)
CapabilitiesNur Hinweis (nicht erzwungen)Zur Laufzeit erzwungen
RessourcenlimitsKeineCPU, Speicher, Subrequests, Wall-Time
NetzwerkzugriffUneingeschränktBlockiert; nur über ctx.http mit Host-Allowlist
DatenzugriffVoller DatenbankzugriffÜber RPC-Bridge auf deklarierte Capabilities beschränkt
Verfügbar aufAllen PlattformenNur Cloudflare Workers

Trusted-Modus

Trusted Plugins laufen im selben Prozess wie deine Astro-Site. Sie werden aus npm-Paketen oder lokalen Dateien geladen und in astro.config.mjs konfiguriert:

import myPlugin from "@emdash-cms/plugin-analytics";

export default defineConfig({
	integrations: [
		emdash({
			plugins: [myPlugin()],
		}),
	],
});

Im Trusted-Modus:

  • Capabilities sind Dokumentation, keine Durchsetzung. Ein Plugin, das ["read:content"] deklariert, kann trotzdem auf alles im Prozess zugreifen. Das Feld capabilities sagt Administratoren, was das Plugin vorhat zu nutzen.
  • Keine Ressourcenlimits. CPU-, Speicher- und Netzwerknutzung sind unbegrenzt. Ein fehlerhaftes Plugin kann die gesamte Anfrage blockieren.
  • Voller Prozesszugriff. Plugins teilen sich die Node.js/Workers-Runtime mit deiner Astro-Site. Sie können beliebige Module importieren, auf Umgebungsvariablen zugreifen und (unter Node.js) das Dateisystem lesen/schreiben.

Sandboxed-Modus (Cloudflare Workers)

Sandboxed Plugins laufen in isolierten V8-Isolates über Cloudflares Dynamic Worker Loader-API. Jedes Plugin erhält sein eigenes Isolate mit erzwungenen Limits.

Um Sandboxing zu aktivieren, konfiguriere den Sandbox-Runner in deiner Astro-Config:

export default defineConfig({
	integrations: [
		emdash({
			sandboxRunner: "@emdash-cms/cloudflare/sandbox",
			sandboxed: [
				{
					manifest: seoPluginManifest,
					code: seoPluginCode,
				},
			],
		}),
	],
});

Was die Sandbox erzwingt

  1. Capability-Durchsetzung

    Deklariert ein Plugin capabilities: ["read:content"], kann es nur ctx.content.get() und ctx.content.list() aufrufen. Ein Versuch mit ctx.content.create() löst einen Berechtigungsfehler aus. Das wird über die RPC-Bridge erzwungen — das Plugin kann das nicht umgehen, weil es keinen direkten Datenbankzugriff hat.

  2. Ressourcenlimits

    Jeder Aufruf (Hook oder Route) läuft mit:

    RessourceStandardErzwungen durch
    CPU-Zeit50msWorker Loader (V8-Isolate)
    Subrequests10 pro AufrufWorker Loader (V8-Isolate)
    Wall-Clock-Zeit30 SekundenEmDash-Runner (Promise.race)
    Speicher~128MBV8-Plattform-Obergrenze (nicht pro Plugin konfigurierbar)

    Überschreitung der CPU- oder Subrequest-Limits bricht das Isolate im Worker Loader ab und wirft eine Exception. Überschreitung des Wall-Time-Limits lässt EmDash das Invocation-Promise ablehnen. Der Speicher ist durch die V8-Obergrenze begrenzt, pro Plugin aber nicht konfigurierbar.

    Das sind die eingebauten Standardwerte. Eigene Limits sind möglich, indem eine eigene SandboxRunnerFactory andere Werte über SandboxOptions.limits übergibt. Pro-Site-Konfiguration über die EmDash-Integration ist noch nicht implementiert.

  3. Netzwerk-Isolation

    Sandboxed Plugins haben globalOutbound: null — direkte fetch()-Aufrufe werden auf V8-Ebene blockiert. Plugins müssen ctx.http.fetch() nutzen, das über die Bridge proxied wird. Die Bridge prüft den Ziel-Host gegen die allowedHosts-Liste des Plugins.

  4. Storage-Scoping

    Alle Storage-Operationen (KV, Collections) sind auf die Plugin-ID beschränkt. Ein Plugin kann nicht die Daten eines anderen Plugins lesen. Content- und Media-Zugriff läuft über die Bridge, die bei jedem Aufruf die Capabilities prüft.

  5. Feature-Einschränkungen

    Einige Features gibt es nur im Trusted-Modus:

    • API routes — Eigene REST-Endpunkte (routes) sind nicht verfügbar. Sandboxed Plugins interagieren über Block-Kit-Admin-Seiten und Hooks.
    • Portable Text block types — PT-Blocks brauchen Astro-Komponenten für das Rendering auf der Site (componentsEntry), zur Build-Zeit aus npm geladen. Sandboxed Plugins werden zur Laufzeit installiert und können keine Komponenten mitliefern.
    • Custom React admin pages — Sandboxed Plugins nutzen Block Kit für die Admin-UI statt eigener React-Komponenten.

    Der Befehl emdash plugin bundle warnt, wenn ein Plugin diese Features deklariert.

Architektur

Sandboxed Plugins kommunizieren mit EmDash über eine RPC-Bridge:

┌─────────────────────┐     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     │
                                     └──────────────┘

Der Plugin-Code läuft in einer V8-Isolate. Er erhält ein ctx-Objekt, bei dem jede Methode ein Proxy zur Bridge ist. Die Bridge läuft im Haupt-EmDash-Worker und führt die eigentlichen DB/Storage-Operationen nach Capability-Prüfung aus.

Wrangler-Konfiguration

Sandboxing erfordert den Dynamic Worker Loader. Ergänze deine wrangler.jsonc:

{
	"worker_loaders": [{ "binding": "LOADER" }],
	"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "emdash-media" }],
	"d1_databases": [{ "binding": "DB", "database_name": "emdash" }]
}

Node.js-Deployments

Bei Deployment auf Node.js (oder jeder Nicht-Cloudflare-Plattform):

  • Es wird NoopSandboxRunner verwendet. Er liefert isAvailable() === false.
  • Das Laden sandboxed Plugins wirft SandboxNotAvailableError.
  • Alle Plugins müssen als Trusted Plugins im Array plugins registriert werden.
  • Capability-Deklarationen sind rein informativ — sie werden nicht erzwungen.

Was das für die Sicherheit bedeutet

BedrohungCloudflare (Sandboxed)Node.js (nur Trusted)
Plugin liest Daten, die es nicht sollBlockiert durch Bridge-Capability-ChecksNicht verhindert — Plugin hat vollen DB-Zugriff
Plugin macht unerlaubte NetzwerkaufrufeBlockiert durch globalOutbound: null + Host-AllowlistNicht verhindert — Plugin kann fetch() direkt aufrufen
Plugin erschöpft CPUIsolate wird vom Worker Loader abgebrochenNicht verhindert — blockiert die Event Loop
Plugin erschöpft SpeicherIsolate wird vom Worker Loader beendetNicht verhindert — kann den Prozess zum Absturz bringen
Plugin greift auf Umgebungsvariablen zuKein Zugriff (isoliertes V8-Kontext)Nicht verhindert — teilt process.env
Plugin greift auf Dateisystem zuKein Dateisystem in WorkersNicht verhindert — voller fs-Zugriff

Empfehlungen für Node.js-Deployments

  1. Nur Plugins aus vertrauenswürdigen Quellen installieren. Prüfe den Quellcode vor der Installation. Bevorzuge Plugins bekannter Maintainer.
  2. Capability-Deklarationen als Review-Checkliste nutzen. Auch ohne Durchsetzung dokumentieren sie den beabsichtigten Umfang. Ein Plugin mit ["network:fetch"], das kein Netz braucht, ist verdächtig.
  3. Ressourcennutzung überwachen. Nutze Monitoring auf Prozessebene (z. B. --max-old-space-size, Health Checks), um fehlerhafte Plugins zu erkennen.
  4. Cloudflare für nicht vertrauenswürdige Plugins erwägen. Wenn du Plugins unbekannter Herkunft brauchst (z. B. aus einem Marketplace), deploye auf Cloudflare Workers, wo Sandboxing verfügbar ist.

Gleiche API, andere Garantien

Der Plugin-Code ist in beiden Modi identisch. Die definePlugin()-API, die Form von ctx, Hooks, Routes und Storage funktionieren gleich. Was sich ändert, ist die Durchsetzung:

// 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 }),
			});
		},
	},
});

Ziel: Plugin-Autoren entwickeln lokal im Trusted-Modus (schnellere Iteration, einfacheres Debugging) und deployen in Production im Sandboxed-Modus — ohne Codeänderungen.