Sandbox dei plugin

In questa pagina

EmDash può eseguire i plugin in due modalità: trusted e sandboxed. Questa pagina spiega come funzionano, quali protezioni offrono e le implicazioni di sicurezza per diversi target di deployment.

Modalità di esecuzione

TrustedSandboxed
Eseguito inProcesso principaleIsolate V8 isolato (Dynamic Worker Loader)
CapabilitiesIndicative (non applicate)Applicate a runtime
Limiti risorseNessunoCPU, memoria, subrequest, tempo reale
Accesso di reteIllimitatoBloccato; solo via ctx.http con allowlist host
Accesso ai datiAccesso completo al databaseLimitato alle capabilities dichiarate tramite bridge RPC
Disponibile suTutte le piattaformeSolo Cloudflare Workers

Modalità trusted

I plugin trusted girano nello stesso processo del sito Astro. Sono caricati da pacchetti npm o file locali e configurati in astro.config.mjs:

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

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

In modalità trusted:

  • Le capabilities sono documentazione, non enforcement. Un plugin che dichiara ["read:content"] può comunque accedere a tutto nel processo. Il campo capabilities indica agli amministratori cosa il plugin intende usare.
  • Nessun limite di risorse. CPU, memoria e rete non sono limitati. Un plugin difettoso può bloccare l’intera richiesta.
  • Accesso completo al processo. I plugin condividono il runtime Node.js/Workers con il sito Astro. Possono importare qualsiasi modulo, leggere le variabili d’ambiente e accedere al filesystem (su Node.js).

Modalità sandboxed (Cloudflare Workers)

I plugin sandboxed girano in isolate V8 isolati forniti dall’API Dynamic Worker Loader di Cloudflare. Ogni plugin ha il proprio isolate con limiti applicati.

Per abilitare il sandboxing, configura il sandbox runner nella config Astro:

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

Cosa impone la sandbox

  1. Enforcement delle capabilities

    Se un plugin dichiara capabilities: ["read:content"], può chiamare solo ctx.content.get() e ctx.content.list(). Tentare ctx.content.create() genera un errore di permesso. È applicato dal bridge RPC — il plugin non può aggirarlo perché non ha accesso diretto al database.

  2. Limiti di risorse

    Ogni invocazione (hook o route) gira con:

    RisorsaPredefinitoApplicato da
    Tempo CPU50msWorker Loader (isolate V8)
    Subrequest10 per invocazioneWorker Loader (isolate V8)
    Tempo reale30 secondiRunner EmDash (Promise.race)
    Memoria~128MBTetto piattaforma V8 (non configurabile per plugin)

    Superare i limiti CPU o subrequest fa abortire l’isolate nel Worker Loader e lancia un’eccezione. Superare il limite di tempo fa rifiutare la promise di invocazione da EmDash. La memoria è limitata dal tetto V8 ma non è configurabile per plugin.

    Questi sono i default integrati. Limiti personalizzati si possono impostare fornendo una SandboxRunnerFactory che passa valori diversi via SandboxOptions.limits. La configurazione per sito nell’integrazione EmDash non è ancora implementata.

  3. Isolamento di rete

    I plugin sandboxed hanno globalOutbound: null — le chiamate fetch() dirette sono bloccate a livello V8. Devono usare ctx.http.fetch(), proxato dal bridge. Il bridge valida l’host di destinazione rispetto alla lista allowedHosts del plugin.

  4. Scoping dello storage

    Tutte le operazioni di storage (KV, collections) sono limitate all’ID del plugin. Un plugin non può leggere i dati di un altro. Accesso a content e media tramite bridge, che controlla le capabilities a ogni chiamata.

  5. Restrizioni di funzionalità

    Alcune funzioni sono disponibili solo in modalità trusted:

    • API routes — Endpoint REST personalizzati (routes) non disponibili. I plugin sandboxed interagiscono tramite pagine admin Block Kit e hook.
    • Tipi di blocco Portable Text — I blocchi PT richiedono componenti Astro per il rendering lato sito (componentsEntry), caricati a build time da npm. I plugin sandboxed si installano a runtime e non possono includere componenti.
    • Pagine admin React personalizzate — I plugin sandboxed usano Block Kit per l’UI admin invece di componenti React.

    Il comando emdash plugin bundle avvisa se un plugin dichiara queste funzionalità.

Architettura

I plugin sandboxed comunicano con EmDash tramite un bridge RPC:

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

Il codice del plugin gira in un isolate V8. Riceve un oggetto ctx dove ogni metodo è un proxy verso il bridge. Il bridge gira nel worker principale EmDash ed esegue le operazioni reali su database/storage dopo la validazione delle capabilities.

Configurazione Wrangler

Il sandboxing richiede Dynamic Worker Loader. Aggiungi a wrangler.jsonc:

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

Deployment Node.js

In deployment su Node.js (o qualsiasi piattaforma non Cloudflare):

  • Si usa NoopSandboxRunner. Restituisce isAvailable() === false.
  • Tentare di caricare plugin sandboxed lancia SandboxNotAvailableError.
  • Tutti i plugin devono essere registrati come trusted nell’array plugins.
  • Le dichiarazioni di capabilities sono solo informative — non sono applicate.

Cosa significa per la sicurezza

MinacciaCloudflare (Sandboxed)Node.js (solo Trusted)
Il plugin legge dati che non dovrebbeBloccato dai controlli capabilities sul bridgeNon impedito — accesso DB completo
Il plugin effettua chiamate di rete non autorizzateBloccato da globalOutbound: null + allowlist hostNon impedito — può chiamare fetch() direttamente
Il plugin esaurisce la CPUIsolate abortito dal Worker LoaderNon impedito — blocca l’event loop
Il plugin esaurisce la memoriaIsolate terminato dal Worker LoaderNon impedito — può far crashare il processo
Il plugin accede alle variabili d’ambienteNessun accesso (contesto V8 isolato)Non impedito — condivide process.env
Il plugin accede al filesystemNessun filesystem in WorkersNon impedito — accesso completo a fs

Raccomandazioni per deployment Node.js

  1. Installa plugin solo da fonti fidate. Revisiona il codice sorgente prima dell’installazione. Preferisci plugin di maintainer noti.
  2. Usa le capabilities come checklist di review. Anche senza enforcement documentano la portata prevista. Un plugin con ["network:fetch"] che non serve rete è sospetto.
  3. Monitora l’uso delle risorse. Usa monitoraggio a livello processo (es. --max-old-space-size, health check) per individuare plugin fuori controllo.
  4. Considera Cloudflare per plugin non attendibili. Se devi eseguire plugin da origini sconosciute (es. marketplace), deploya su Cloudflare Workers dove il sandboxing è disponibile.

Stessa API, garanzie diverse

Il codice del plugin è identico in entrambe le modalità. L’API definePlugin(), la forma di ctx, hooks, routes e storage funzionano allo stesso modo. Ciò che cambia è l’enforcement:

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

L’obiettivo è permettere agli autori di sviluppare in locale in modalità trusted (iterazione più veloce, debug più semplice) e deployare in produzione in modalità sandboxed senza modificare il codice.