Aller au contenu

Bac à sable des plugins

EmDash prend en charge l’exécution de plugins dans deux modes d’exécution : de confiance et sandboxé. Cette page explique comment fonctionne chaque mode, quelles protections ils offrent et les implications en matière de sécurité pour différentes cibles de déploiement.

De confianceSandboxé
S’exécute dansProcessus principalIsolat V8 isolé (Dynamic Worker Loader)
CapacitésConsultatives (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’autorisation d’hôtes
Accès aux donnéesAccès complet à la base de donnéesLimité aux capacités déclarées via le pont RPC
Disponible surToutes les plateformesCloudflare Workers uniquement

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

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

En mode de confiance :

  • Les capacités sont documentaires, non contraignantes. Un plugin déclarant ["read:content"] peut toujours accéder à tout dans le processus. Le champ capabilities indique aux administrateurs ce que le plugin prévoit d’utiliser.
  • Aucune limite de ressources. L’utilisation du CPU, de la mémoire et du réseau est illimitée. Un plugin défaillant peut bloquer l’intégralité de la requête.
  • Accès complet au processus. Les plugins partagent l’environnement d’exécution Node.js/Workers avec votre site Astro. Ils peuvent importer n’importe quel module, accéder aux variables d’environnement et lire/écrire sur le système de fichiers (sur Node.js).

Les plugins sandboxés s’exécutent dans des isolats V8 isolés fournis par l’API Dynamic Worker Loader de Cloudflare. Chaque plugin obtient son propre isolat avec des limites appliquées.

Pour activer le sandboxing, configurez le moteur sandbox dans votre configuration Astro :

typescript title="astro.config.mjs"
export default defineConfig({
integrations: [
emdash({
sandboxRunner: "@emdash-cms/cloudflare/sandbox",
sandboxed: [
{
manifest: seoPluginManifest,
code: seoPluginCode,
},
],
}),
],
});
  1. Application des capacités

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

  2. Limites de ressources

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

    RessourcePar défautAppliqué par
    Temps CPU50msWorker Loader (isolat V8)
    Sous-requêtes10 par invocationWorker Loader (isolat V8)
    Temps réel30 secondesMoteur EmDash (Promise.race)
    Mémoire~128MBPlafond de la plateforme V8 (non configurable par plugin)

    Dépasser les limites de CPU ou de sous-requêtes amène le Worker Loader à interrompre l’isolat et à lever une exception. Dépasser la limite de temps réel amène EmDash à rejeter la promesse d’invocation. La mémoire est limitée par le plafond de la plateforme V8 mais ne peut être configurée par plugin.

    Ce sont les valeurs par défaut intégrées. Des limites personnalisées peuvent être configurées en fournissant une SandboxRunnerFactory personnalisée qui transmet différentes valeurs via SandboxOptions.limits. La configuration par site via la configuration d’intégration EmDash n’est pas encore implémentée.

  3. Isolation réseau

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

  4. Délimitation du stockage

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

  5. Restrictions de fonctionnalités

    Certaines fonctionnalités ne sont disponibles qu’en mode de confiance :

    • Routes API — Les points de terminaison REST personnalisés (routes) ne sont pas disponibles. Les plugins sandboxés interagissent avec les utilisateurs via les pages d’administration 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 moment de la construction depuis npm. Les plugins sandboxés sont installés à l’exécution et ne peuvent pas fournir de composants.
    • Pages d’administration React personnalisées — Les plugins sandboxés utilisent Block Kit pour l’interface d’administration au lieu de fournir des composants React.

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

Les plugins sandboxés 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 s’exécute dans un isolat V8. Il reçoit un objet ctx où chaque méthode est un proxy vers le pont. Le pont s’exécute dans le worker principal d’EmDash et effectue les opérations réelles de base de données/stockage après validation des capacités.

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

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

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

  • Le NoopSandboxRunner est utilisé. Il renvoie isAvailable() === false.
  • Tenter de charger des plugins sandboxés génère une SandboxNotAvailableError.
  • Tous les plugins doivent être enregistrés en tant que plugins de confiance dans le tableau plugins.
  • Les déclarations de capacités sont purement informatives — elles ne sont pas appliquées.
MenaceCloudflare (Sandboxé)Node.js (De confiance uniquement)
Le plugin lit des données qu’il ne devrait pasBloqué par les vérifications de capacités du pontNon empêché — le plugin a un accès complet à la base de données
Le plugin effectue des appels réseau non autorisésBloqué par globalOutbound: null + liste d’autorisation d’hôtesNon empêché — le plugin peut appeler fetch() directement
Le plugin épuise le CPUIsolat interrompu par le Worker LoaderNon empêché — bloque la boucle d’événements
Le plugin épuise la mémoireIsolat terminé par le Worker LoaderNon empêché — peut faire planter le processus
Le plugin accède aux variables d’environnementAucun accès (contexte V8 isolé)Non empêché — partage process.env
Le plugin accède au système de fichiersAucun système de fichiers dans WorkersNon empêché — accès complet à fs
  1. N’installez que des plugins provenant de sources de confiance. Examinez le code source de tout plugin avant de l’installer. Privilégiez les plugins publiés par des mainteneurs connus.
  2. Utilisez les déclarations de capacités comme liste de contrôle pour la revue. Même si les capacités ne sont pas appliquées, elles documentent la portée prévue du plugin. Un plugin déclarant ["network:fetch"] qui n’a pas besoin d’accès réseau est suspect.
  3. Surveillez l’utilisation des ressources. Utilisez une surveillance au niveau du processus (par ex., --max-old-space-size, contrôles de santé) pour détecter les plugins incontrôlables.
  4. Envisagez Cloudflare pour les plugins non fiables. Si vous devez exécuter des plugins provenant de sources inconnues (par ex., une marketplace), déployez-les sur Cloudflare Workers où le sandboxing est disponible.

Le code d’un plugin est identique quel que soit le mode d’exécution. L’API definePlugin(), la forme du contexte, les hooks, les routes et le stockage fonctionnent tous de la même manière. 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 plugins de développer localement en mode de confiance (itération plus rapide, débogage plus facile) et de déployer en mode sandboxé en production sans modifier le code.