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
| Trusted | Sandboxed | |
|---|---|---|
| S’exécute dans | Processus principal | Isolate V8 isolé (Dynamic Worker Loader) |
| Capabilities | Indicatives (non appliquées) | Appliquées à l’exécution |
| Limites de ressources | Aucune | CPU, mémoire, sous-requêtes, temps réel |
| Accès réseau | Illimité | Bloqué ; uniquement via ctx.http avec liste d’hôtes autorisés |
| Accès aux données | Accès base de données complet | Limité aux capabilities déclarées via pont RPC |
| Disponible sur | Toutes plateformes | Cloudflare 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 champcapabilitiesindique 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
-
Application des capabilities
Si un plugin déclare
capabilities: ["read:content"], il ne peut appeler quectx.content.get()etctx.content.list(). Tenterctx.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. -
Limites de ressources
Chaque invocation (hook ou route) s’exécute avec :
Ressource Défaut Appliqué par Temps CPU 50ms Worker Loader (isolate V8) Sous-requêtes 10 par invocation Worker Loader (isolate V8) Temps réel 30 secondes Runner EmDash ( Promise.race)Mémoire ~128MB Plafond 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
SandboxRunnerFactoryqui passe d’autres valeurs dansSandboxOptions.limits. La configuration par site dans l’intégration EmDash n’est pas encore implémentée. -
Isolation réseau
Les plugins sandboxed ont
globalOutbound: null— les appelsfetch()directs sont bloqués au niveau V8. Ils doivent utiliserctx.http.fetch(), proxifié par le pont. Le pont valide l’hôte cible contre la listeallowedHostsdu plugin. -
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.
-
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 bundleavertit si un plugin déclare ces fonctionnalités. - API routes — Les endpoints REST personnalisé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) :
NoopSandboxRunnerest utilisé. Il renvoieisAvailable() === 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é
| Menace | Cloudflare (Sandboxed) | Node.js (Trusted uniquement) |
|---|---|---|
| Le plugin lit des données qu’il ne devrait pas | Bloqué par les contrôles de capabilities du pont | Non empêché — accès BD complet |
| Le plugin effectue des appels réseau non autorisés | Bloqué par globalOutbound: null + liste d’hôtes | Non empêché — peut appeler fetch() directement |
| Le plugin épuise le CPU | Isolate aborté par le Worker Loader | Non empêché — bloque la boucle d’événements |
| Le plugin épuise la mémoire | Isolate terminé par le Worker Loader | Non empêché — peut faire planter le processus |
| Le plugin accède aux variables d’environnement | Pas d’accès (contexte V8 isolé) | Non empêché — partage process.env |
| Le plugin accède au système de fichiers | Pas de FS dans Workers | Non empêché — accès fs complet |
Recommandations pour les déploiements Node.js
- N’installez des plugins que depuis des sources de confiance. Auditez le code source avant installation. Préférez les plugins de mainteneurs connus.
- 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. - 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. - 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.