Sandbox de plugins

En esta página

EmDash puede ejecutar plugins en dos modos: trusted y sandboxed. Esta página explica cómo funciona cada uno, qué protecciones ofrecen y las implicaciones de seguridad según el destino de despliegue.

Modos de ejecución

TrustedSandboxed
Se ejecuta enProceso principalAislamiento V8 aislado (Dynamic Worker Loader)
CapabilitiesOrientativas (no se aplican)Aplicadas en tiempo de ejecución
Límites de recursosNingunoCPU, memoria, subrequests, tiempo real
Acceso a redSin restriccionesBloqueado; solo vía ctx.http con lista de hosts permitidos
Acceso a datosAcceso completo a la base de datosLimitado a capabilities declaradas vía puente RPC
Disponible enTodas las plataformasSolo Cloudflare Workers

Modo trusted

Los plugins trusted se ejecutan en el mismo proceso que tu sitio Astro. Se cargan desde paquetes npm o archivos locales y se configuran en astro.config.mjs:

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

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

En modo trusted:

  • Las capabilities son documentación, no aplicación. Un plugin que declara ["read:content"] aún puede acceder a todo en el proceso. El campo capabilities indica a los administradores qué el plugin pretende usar.
  • Sin límites de recursos. El uso de CPU, memoria y red no está acotado. Un plugin defectuoso puede bloquear toda la petición.
  • Acceso completo al proceso. Los plugins comparten el runtime Node.js/Workers con tu sitio Astro. Pueden importar cualquier módulo, acceder a variables de entorno y leer/escribir el sistema de archivos (en Node.js).

Modo sandboxed (Cloudflare Workers)

Los plugins sandboxed se ejecutan en aislamientos V8 aislados proporcionados por la API Dynamic Worker Loader de Cloudflare. Cada plugin obtiene su propio aislamiento con límites aplicados.

Para habilitar el sandboxing, configura el sandbox runner en tu config de Astro:

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

Qué impone el sandbox

  1. Aplicación de capabilities

    Si un plugin declara capabilities: ["read:content"], solo puede llamar a ctx.content.get() y ctx.content.list(). Intentar ctx.content.create() lanza un error de permisos. Esto lo aplica el puente RPC: el plugin no puede eludirlo porque no tiene acceso directo a la base de datos.

  2. Límites de recursos

    Cada invocación (hook o ruta) se ejecuta con:

    RecursoValor por defectoAplicado por
    Tiempo de CPU50msWorker Loader (aislamiento V8)
    Subrequests10 por invocaciónWorker Loader (aislamiento V8)
    Tiempo de reloj30 segundosRunner de EmDash (Promise.race)
    Memoria~128MBTecho de la plataforma V8 (no configurable por plugin)

    Superar los límites de CPU o subrequests hace que el Worker Loader aborte el aislamiento y lance una excepción. Superar el límite de tiempo hace que EmDash rechace la promesa de invocación. La memoria está acotada por el techo de V8 pero no es configurable por plugin.

    Estos son los valores por defecto integrados. Se pueden configurar límites personalizados proporcionando una SandboxRunnerFactory que pase otros valores vía SandboxOptions.limits. La configuración por sitio en la integración de EmDash aún no está implementada.

  3. Aislamiento de red

    Los plugins sandboxed tienen globalOutbound: null: las llamadas directas a fetch() están bloqueadas a nivel V8. Deben usar ctx.http.fetch(), que se proxifica a través del puente. El puente valida el host de destino frente a la lista allowedHosts del plugin.

  4. Ámbito del almacenamiento

    Todas las operaciones de almacenamiento (KV, colecciones) están acotadas al ID del plugin. Un plugin no puede leer los datos de otro. El acceso a contenido y medios pasa por el puente, que comprueba las capabilities en cada llamada.

  5. Restricciones de funcionalidad

    Algunas funciones solo existen en modo trusted:

    • API routes — Los endpoints REST personalizados (routes) no están disponibles. Los plugins sandboxed interactúan mediante páginas admin de Block Kit y hooks.
    • Tipos de bloque Portable Text — Los bloques PT requieren componentes Astro para el render en el sitio (componentsEntry), cargados en build desde npm. Los plugins sandboxed se instalan en runtime y no pueden incluir componentes.
    • Páginas admin React personalizadas — Los plugins sandboxed usan Block Kit para la UI admin en lugar de enviar componentes React.

    El comando emdash plugin bundle advierte si un plugin declara estas características.

Arquitectura

Los plugins sandboxed se comunican con EmDash a través de un puente 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     │
                                     └──────────────┘

El código del plugin corre en un aislamiento V8. Recibe un objeto ctx donde cada método es un proxy al puente. El puente corre en el worker principal de EmDash y realiza las operaciones reales de base de datos/almacenamiento tras validar capabilities.

Configuración de Wrangler

El sandboxing requiere Dynamic Worker Loader. Añade a tu wrangler.jsonc:

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

Despliegues en Node.js

Al desplegar en Node.js (o cualquier plataforma que no sea Cloudflare):

  • Se usa NoopSandboxRunner. Devuelve isAvailable() === false.
  • Intentar cargar plugins sandboxed lanza SandboxNotAvailableError.
  • Todos los plugins deben registrarse como trusted en el array plugins.
  • Las declaraciones de capabilities son solo informativas: no se aplican.

Qué implica para la seguridad

AmenazaCloudflare (Sandboxed)Node.js (solo Trusted)
El plugin lee datos que no deberíaBloqueado por comprobaciones de capabilities en el puenteNo evitado — acceso completo a la BD
El plugin hace llamadas de red no autorizadasBloqueado por globalOutbound: null + lista de hostsNo evitado — puede llamar a fetch() directamente
El plugin agota la CPUEl Worker Loader aborta el aislamientoNo evitado — bloquea el event loop
El plugin agota la memoriaEl Worker Loader termina el aislamientoNo evitado — puede tumbar el proceso
El plugin accede a variables de entornoSin acceso (contexto V8 aislado)No evitado — comparte process.env
El plugin accede al sistema de archivosSin sistema de archivos en WorkersNo evitado — acceso completo a fs

Recomendaciones para despliegues en Node.js

  1. Instala plugins solo de fuentes de confianza. Revisa el código fuente antes de instalar. Prefiere plugins de maintainers conocidos.
  2. Usa las capabilities como lista de revisión. Aunque no se apliquen, documentan el alcance pretendido. Un plugin con ["network:fetch"] que no necesita red es sospechoso.
  3. Supervisa el uso de recursos. Usa monitorización a nivel de proceso (p. ej. --max-old-space-size, health checks) para detectar plugins descontrolados.
  4. Valora Cloudflare para plugins no confiables. Si necesitas ejecutar plugins de origen desconocido (p. ej. marketplace), despliega en Cloudflare Workers, donde el sandboxing está disponible.

Misma API, distintas garantías

El código del plugin es idéntico en ambos modos. La API definePlugin(), la forma de ctx, hooks, routes y storage funcionan igual. Lo que cambia es la aplicación:

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

El objetivo es que los autores desarrollen en local en modo trusted (iteración más rápida, depuración más sencilla) y desplieguen en producción en modo sandboxed sin cambiar código.