Zum Inhalt springen

Plugin-Sandbox

EmDash unterstützt zwei Ausführungsmodi für Plugins: vertrauenswürdig und sandboxed. Diese Seite erklärt, wie beide Modi funktionieren, welche Schutzmechanismen sie bieten und welche Sicherheitsfolgen sich je nach Zielplattform ergeben.

VertrauenswürdigSandboxed
Läuft inHauptprozessIsolierter V8-Isolate (Dynamic Worker Loader)
FähigkeitenInformativ (nicht erzwungen)Zur Laufzeit erzwungen
RessourcenlimitsKeineCPU, Speicher, Subrequests, Laufzeit
NetzwerkzugriffUneingeschränktBlockiert; nur über ctx.http mit Host-Allowlist
DatenzugriffVollständiger DatenbankzugriffAuf deklarierte Fähigkeiten beschränkt via RPC-Bridge
Verfügbar aufAllen PlattformenNur Cloudflare Workers

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

astro.config.mjs
import myPlugin from "@emdash-cms/plugin-analytics";
export default defineConfig({
integrations: [
emdash({
plugins: [myPlugin()],
}),
],
});

Im vertrauenswürdigen Modus:

  • Fähigkeiten sind Dokumentation, keine Durchsetzung. Ein Plugin, das ["read:content"] deklariert, kann trotzdem auf alles im Prozess zugreifen. Das Feld capabilities zeigt Administratoren nur, was das Plugin voraussichtlich nutzen will.
  • Keine Ressourcenlimits. CPU-, Speicher- und Netzwerknutzung sind unbegrenzt. Ein fehlerhaftes Plugin kann die gesamte Anfrage blockieren.
  • Voller Prozesszugriff. Plugins teilen sich die Node.js- oder Workers-Laufzeit mit Ihrer Astro-Site. Sie können beliebige Module importieren, auf Umgebungsvariablen zugreifen und unter Node.js auf das Dateisystem lesen und schreiben.

Sandboxed-Plugins laufen in isolierten V8-Isolates, die von der Dynamic Worker Loader-API von Cloudflare bereitgestellt werden. Jedes Plugin erhält seine eigene Laufzeitumgebung mit erzwungenen Limits.

Um Sandboxing zu aktivieren, konfigurieren Sie den Sandbox-Runner in Ihrer Astro-Konfiguration:

astro.config.mjs
export default defineConfig({
integrations: [
emdash({
sandboxRunner: "@emdash-cms/cloudflare/sandbox",
sandboxed: [
{
manifest: seoPluginManifest,
code: seoPluginCode,
},
],
}),
],
});
  1. Fähigkeitserzwingung

    Wenn ein Plugin capabilities: ["read:content"] deklariert, kann es nur ctx.content.get() und ctx.content.list() aufrufen. Der Versuch, ctx.content.create() aufzurufen, löst einen Berechtigungsfehler aus. Das wird von der RPC-Bridge durchgesetzt, und das Plugin kann es nicht umgehen, weil es keinen direkten Datenbankzugriff hat.

  2. Ressourcenlimits

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

    RessourceStandardErzwungen durch
    CPU-Zeit50msWorker Loader (V8-Isolate)
    Subrequests10 pro AufrufWorker Loader (V8-Isolate)
    Gesamtzeit30 SekundenEmDash-Runner (Promise.race)
    Speicher~128MBV8-Plattformlimit (nicht pro Plugin konfigurierbar)

    Wenn CPU- oder Subrequest-Limits überschritten werden, bricht der Worker Loader den Isolate ab und wirft eine Exception. Wird die Gesamtzeit überschritten, lehnt EmDash das Aufruf-Promise ab. Der Speicher ist durch das V8-Plattformlimit begrenzt und kann nicht pro Plugin konfiguriert werden.

    Dies sind die eingebauten Standardwerte. Eigene Limits lassen sich mit einer benutzerdefinierten SandboxRunnerFactory setzen, die andere Werte in SandboxOptions.limits übergibt. Eine Konfiguration pro Site über die EmDash-Integration ist bisher nicht implementiert.

  3. Netzwerkisolation

    Sandboxed-Plugins haben globalOutbound: null, daher sind direkte fetch()-Aufrufe auf V8-Ebene blockiert. Plugins müssen ctx.http.fetch() verwenden, das über die Bridge läuft. Die Bridge validiert den Zielhost gegen die allowedHosts-Liste des Plugins.

  4. Speicherbereichsbegrenzung

    Alle Speicheroperationen wie KV und Collections sind auf die Plugin-ID beschränkt. Ein Plugin kann nicht die Daten eines anderen Plugins lesen. Der Zugriff auf Inhalte und Medien läuft über die Bridge, die bei jedem Aufruf die Fähigkeiten prüft.

  5. Funktionseinschränkungen

    Einige Funktionen sind nur im vertrauenswürdigen Modus verfügbar:

    • API-Routen – Benutzerdefinierte REST-Endpunkte (routes) sind nicht verfügbar. Sandboxed-Plugins interagieren mit Benutzern über Block-Kit-Adminseiten und Hooks.
    • Portable-Text-Blocktypen – PT-Blöcke benötigen Astro-Komponenten für das Rendering auf der Website (componentsEntry), die zur Build-Zeit aus npm geladen werden. Sandboxed-Plugins werden zur Laufzeit installiert und können keine Komponenten mitbringen.
    • Benutzerdefinierte React-Adminseiten – Sandboxed-Plugins verwenden Block Kit für die Admin-UI anstelle von mitgelieferten React-Komponenten.

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

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 Code des Plugins läuft in einem V8-Isolate. Er erhält ein ctx-Objekt, bei dem jede Methode als Proxy zur Bridge dient. Die Bridge läuft im Haupt-Worker von EmDash und führt nach der Prüfung der Fähigkeiten die eigentlichen Datenbank- und Speicheroperationen aus.

Sandboxing erfordert Dynamic Worker Loader. Fügen Sie zu Ihrer wrangler.jsonc hinzu:

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

Bei der Bereitstellung auf Node.js (oder einer beliebigen Nicht-Cloudflare-Plattform):

  • Der NoopSandboxRunner wird verwendet. Er gibt isAvailable() === false zurück.
  • Der Versuch, Sandboxed-Plugins zu laden, löst SandboxNotAvailableError aus.
  • Alle Plugins müssen als vertrauenswürdige Plugins im plugins-Array registriert sein.
  • Fähigkeitsdeklarationen sind rein informativ – sie werden nicht erzwungen.
BedrohungCloudflare (sandboxed)Node.js (nur vertrauenswürdig)
Plugin liest Daten, die es nicht sollteDurch Capability-Prüfungen in der Bridge blockiertNicht verhindert — Plugin hat vollen DB-Zugriff
Plugin führt nicht autorisierte Netzwerkaufrufe durchDurch globalOutbound: null und Host-Allowlist blockiertNicht verhindert — Plugin kann fetch() direkt aufrufen
Plugin erschöpft CPUIsolate wird durch Worker Loader abgebrochenNicht verhindert — blockiert die Event Loop
Plugin erschöpft SpeicherIsolate wird durch Worker Loader beendetNicht verhindert — kann den Prozess abstürzen lassen
Plugin greift auf Umgebungsvariablen zuKein Zugriff (isolierter V8-Kontext)Nicht verhindert — teilt sich process.env
Plugin greift auf das Dateisystem zuKein Dateisystem in WorkersNicht verhindert — voller fs-Zugriff
  1. Installieren Sie Plugins nur aus vertrauenswürdigen Quellen. Überprüfen Sie den Quellcode eines Plugins vor der Installation. Bevorzugen Sie Plugins, die von bekannten Maintainern veröffentlicht werden.
  2. Nutzen Sie Capability-Deklarationen als Prüfliste. Auch wenn Capabilities nicht durchgesetzt werden, dokumentieren sie den beabsichtigten Umfang des Plugins. Ein Plugin, das ["network:fetch"] deklariert, aber keinen Netzwerkzugriff benötigt, ist verdächtig.
  3. Überwachen Sie die Ressourcennutzung. Verwenden Sie Monitoring auf Prozessebene, zum Beispiel --max-old-space-size und Health Checks, um aus dem Ruder laufende Plugins zu erkennen.
  4. Erwägen Sie Cloudflare für nicht vertrauenswürdige Plugins. Wenn Sie Plugins aus unbekannten Quellen ausführen müssen, etwa aus einem Marketplace, stellen Sie sie auf Cloudflare Workers bereit, wo Sandboxing verfügbar ist.

Der Code eines Plugins ist unabhängig vom Ausführungsmodus identisch. Die definePlugin()-API, die Kontextform, Hooks, Routen und Storage funktionieren alle auf die gleiche Weise. Was sich ändert, ist die Durchsetzung:

// Dieses Plugin funktioniert sowohl im vertrauenswürdigen als auch im sandboxed Modus
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) => {
// Im vertrauenswürdigen Modus ist ctx.http immer vorhanden, weil Capabilities nicht erzwungen werden
// Im sandboxed Modus ist ctx.http vorhanden, weil "network:fetch" deklariert wurde
await ctx.http.fetch("https://api.analytics.example.com/track", {
method: "POST",
body: JSON.stringify({ contentId: event.content.id }),
});
},
},
});

Das Ziel ist es, Plugin-Autoren die lokale Entwicklung im vertrauenswürdigen Modus zu ermöglichen, mit schnellerer Iteration und einfacherem Debugging, und dieselben Plugins ohne Codeänderungen im Sandboxed-Modus in Produktion bereitzustellen.