Bac à sable des plugins

Sur cette page

EmDash peut exécuter les plugins selon deux modes : trusted et sandboxed. Cette page explique le fonctionnement de chaque mode, les protections offertes et les implications de sécurité selon la cible de déploiement.

Modes d’exécution

TrustedSandboxed
S’exécute dansProcessus principalIsolate V8 isolé (Dynamic Worker Loader)
CapabilitiesIndicatives (non appliquées)Appliquées à l’exécution
Limites de ressourcesAucuneCPU, mémoire, sous-requêtes, temps réel
Accès réseauIllimitéBloqué ; uniquement via ctx.http avec liste d’hôtes autorisés
Accès aux donnéesAccès base de données completLimité aux capabilities déclarées via pont RPC
Disponible surToutes plateformesCloudflare Workers uniquement

Mode trusted

Les plugins trusted s’exécutent dans le même processus que votre site Astro. Ils sont chargés depuis des paquets npm ou des fichiers locaux et configurés dans astro.config.mjs :

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

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

En mode trusted :

  • Les capabilities sont de la documentation, pas une application. Un plugin déclarant ["read:content"] peut tout de même accéder à tout dans le processus. Le champ capabilities indique aux administrateurs ce que le plugin prévoit d’utiliser.
  • Pas de limites de ressources. L’usage CPU, mémoire et réseau n’est pas borné. Un plugin défaillant peut bloquer toute la requête.
  • Accès processus complet. Les plugins partagent le runtime Node.js/Workers avec votre site Astro. Ils peuvent importer n’importe quel module, lire les variables d’environnement et accéder au système de fichiers (sous Node.js).

Mode sandboxed (Cloudflare Workers)

Les plugins sandboxed s’exécutent dans des isolates V8 isolés fournis par l’API Dynamic Worker Loader de Cloudflare. Chaque plugin dispose de son propre isolate avec des limites appliquées.

Pour activer le sandboxing, configurez le sandbox runner dans votre config Astro :

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

Ce que le sandbox impose

  1. Application des capabilities

    Si un plugin déclare capabilities: ["read:content"], il ne peut appeler que ctx.content.get() et ctx.content.list(). Tenter ctx.content.create() lève une erreur de permission. C’est appliqué par le pont RPC — le plugin ne peut pas contourner car il n’a pas d’accès direct à la base.

  2. Limites de ressources

    Chaque invocation (hook ou route) s’exécute avec :

    RessourceDéfautAppliqué par
    Temps CPU50msWorker Loader (isolate V8)
    Sous-requêtes10 par invocationWorker Loader (isolate V8)
    Temps réel30 secondesRunner EmDash (Promise.race)
    Mémoire~128MBPlafond plateforme V8 (non configurable par plugin)

    Dépassement des limites CPU ou sous-requêtes : le Worker Loader abort l’isolate et lève une exception. Dépassement du temps réel : EmDash rejette la promesse d’invocation. La mémoire est bornée par le plafond V8 mais n’est pas configurable par plugin.

    Ce sont les valeurs par défaut intégrées. Des limites personnalisées sont possibles via une SandboxRunnerFactory qui passe d’autres valeurs dans SandboxOptions.limits. La configuration par site dans l’intégration EmDash n’est pas encore implémentée.

  3. Isolation réseau

    Les plugins sandboxed ont globalOutbound: null — les appels fetch() directs sont bloqués au niveau V8. Ils doivent utiliser ctx.http.fetch(), proxifié par le pont. Le pont valide l’hôte cible contre la liste allowedHosts du plugin.

  4. Périmètre du stockage

    Toutes les opérations de stockage (KV, collections) sont limitées à l’ID du plugin. Un plugin ne peut pas lire les données d’un autre. L’accès contenu et médias passe par le pont, qui vérifie les capabilities à chaque appel.

  5. Restrictions de fonctionnalités

    Certaines fonctionnalités n’existent qu’en mode trusted :

    • API routes — Les endpoints REST personnalisés (routes) ne sont pas disponibles. Les plugins sandboxed interagissent via les pages admin Block Kit et les hooks.
    • Types de blocs Portable Text — Les blocs PT nécessitent des composants Astro pour le rendu côté site (componentsEntry), chargés au build depuis npm. Les plugins sandboxed s’installent au runtime et ne peuvent pas embarquer de composants.
    • Pages admin React personnalisées — Les plugins sandboxed utilisent Block Kit pour l’UI admin au lieu d’envoyer des composants React.

    La commande emdash plugin bundle avertit si un plugin déclare ces fonctionnalités.

Architecture

Les plugins sandboxed communiquent avec EmDash via un pont 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     │
                                     └──────────────┘

Le code du plugin tourne dans un isolate V8. Il reçoit un objet ctx où chaque méthode est un proxy vers le pont. Le pont tourne dans le worker EmDash principal et effectue les opérations base/stockage après validation des capabilities.

Configuration Wrangler

Le sandboxing nécessite Dynamic Worker Loader. Ajoutez à votre wrangler.jsonc :

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

Déploiements Node.js

Lors d’un déploiement sur Node.js (ou toute plateforme non Cloudflare) :

  • NoopSandboxRunner est utilisé. Il renvoie isAvailable() === false.
  • Tenter de charger des plugins sandboxed lève SandboxNotAvailableError.
  • Tous les plugins doivent être enregistrés comme trusted dans le tableau plugins.
  • Les déclarations de capabilities sont purement informatives — elles ne sont pas appliquées.

Conséquences pour la sécurité

MenaceCloudflare (Sandboxed)Node.js (Trusted uniquement)
Le plugin lit des données qu’il ne devrait pasBloqué par les contrôles de capabilities du pontNon empêché — accès BD complet
Le plugin effectue des appels réseau non autorisésBloqué par globalOutbound: null + liste d’hôtesNon empêché — peut appeler fetch() directement
Le plugin épuise le CPUIsolate aborté par le Worker LoaderNon empêché — bloque la boucle d’événements
Le plugin épuise la mémoireIsolate terminé par le Worker LoaderNon empêché — peut faire planter le processus
Le plugin accède aux variables d’environnementPas d’accès (contexte V8 isolé)Non empêché — partage process.env
Le plugin accède au système de fichiersPas de FS dans WorkersNon empêché — accès fs complet

Recommandations pour les déploiements Node.js

  1. N’installez des plugins que depuis des sources de confiance. Auditez le code source avant installation. Préférez les plugins de mainteneurs connus.
  2. Utilisez les capabilities comme checklist de revue. Même non appliquées, elles documentent la portée prévue. Un plugin déclarant ["network:fetch"] sans besoin réseau est suspect.
  3. Surveillez l’usage des ressources. Utilisez une supervision au niveau processus (ex. --max-old-space-size, health checks) pour détecter les plugins déréglés.
  4. Envisagez Cloudflare pour les plugins non fiables. Si vous devez exécuter des plugins d’origine inconnue (ex. marketplace), déployez sur Cloudflare Workers où le sandboxing est disponible.

Même API, garanties différentes

Le code du plugin est identique quel que soit le mode. L’API definePlugin(), la forme de ctx, hooks, routes et storage fonctionnent de la même façon. Ce qui change, c’est l’application :

// 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’objectif est de permettre aux auteurs de développer en local en mode trusted (itération plus rapide, débogage plus simple) et de déployer en production en mode sandboxed sans modifier le code.