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
| Trusted | Sandboxed | |
|---|---|---|
| Läuft in | Hauptprozess | Isolierte V8-Isolate (Dynamic Worker Loader) |
| Capabilities | Nur Hinweis (nicht erzwungen) | Zur Laufzeit erzwungen |
| Ressourcenlimits | Keine | CPU, Speicher, Subrequests, Wall-Time |
| Netzwerkzugriff | Uneingeschränkt | Blockiert; nur über ctx.http mit Host-Allowlist |
| Datenzugriff | Voller Datenbankzugriff | Über RPC-Bridge auf deklarierte Capabilities beschränkt |
| Verfügbar auf | Allen Plattformen | Nur 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 Feldcapabilitiessagt 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
-
Capability-Durchsetzung
Deklariert ein Plugin
capabilities: ["read:content"], kann es nurctx.content.get()undctx.content.list()aufrufen. Ein Versuch mitctx.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. -
Ressourcenlimits
Jeder Aufruf (Hook oder Route) läuft mit:
Ressource Standard Erzwungen durch CPU-Zeit 50ms Worker Loader (V8-Isolate) Subrequests 10 pro Aufruf Worker Loader (V8-Isolate) Wall-Clock-Zeit 30 Sekunden EmDash-Runner ( Promise.race)Speicher ~128MB V8-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
SandboxRunnerFactoryandere Werte überSandboxOptions.limitsübergibt. Pro-Site-Konfiguration über die EmDash-Integration ist noch nicht implementiert. -
Netzwerk-Isolation
Sandboxed Plugins haben
globalOutbound: null— direktefetch()-Aufrufe werden auf V8-Ebene blockiert. Plugins müssenctx.http.fetch()nutzen, das über die Bridge proxied wird. Die Bridge prüft den Ziel-Host gegen dieallowedHosts-Liste des Plugins. -
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.
-
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 bundlewarnt, wenn ein Plugin diese Features deklariert. - API routes — Eigene REST-Endpunkte (
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
NoopSandboxRunnerverwendet. Er liefertisAvailable() === false. - Das Laden sandboxed Plugins wirft
SandboxNotAvailableError. - Alle Plugins müssen als Trusted Plugins im Array
pluginsregistriert werden. - Capability-Deklarationen sind rein informativ — sie werden nicht erzwungen.
Was das für die Sicherheit bedeutet
| Bedrohung | Cloudflare (Sandboxed) | Node.js (nur Trusted) |
|---|---|---|
| Plugin liest Daten, die es nicht soll | Blockiert durch Bridge-Capability-Checks | Nicht verhindert — Plugin hat vollen DB-Zugriff |
| Plugin macht unerlaubte Netzwerkaufrufe | Blockiert durch globalOutbound: null + Host-Allowlist | Nicht verhindert — Plugin kann fetch() direkt aufrufen |
| Plugin erschöpft CPU | Isolate wird vom Worker Loader abgebrochen | Nicht verhindert — blockiert die Event Loop |
| Plugin erschöpft Speicher | Isolate wird vom Worker Loader beendet | Nicht verhindert — kann den Prozess zum Absturz bringen |
| Plugin greift auf Umgebungsvariablen zu | Kein Zugriff (isoliertes V8-Kontext) | Nicht verhindert — teilt process.env |
| Plugin greift auf Dateisystem zu | Kein Dateisystem in Workers | Nicht verhindert — voller fs-Zugriff |
Empfehlungen für Node.js-Deployments
- Nur Plugins aus vertrauenswürdigen Quellen installieren. Prüfe den Quellcode vor der Installation. Bevorzuge Plugins bekannter Maintainer.
- 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. - Ressourcennutzung überwachen. Nutze Monitoring auf Prozessebene (z. B.
--max-old-space-size, Health Checks), um fehlerhafte Plugins zu erkennen. - 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.