Capabilities y seguridad

En esta página

Los plugins en sandbox están aislados por defecto. Para hacer cualquier cosa más allá de leer y escribir su propio KV y storage, un plugin debe declarar una capability en su manifest. El puente de sandbox controla cada API proporcionada por el host según esas declaraciones — un plugin que no declaró content:read no obtiene un ctx.content, y uno que no declaró network:request no obtiene ctx.http.

Esta página cubre qué otorga cada capability, cómo la sandbox las hace cumplir y qué no es aplicable.

Declarar capabilities

Las capabilities viven en emdash-plugin.jsonc, junto con slug y el resto del contrato de confianza:

{
	"slug": "plugin-hello",
	// ...identity + profile...

	"capabilities": ["content:read", "network:request"],
	"allowedHosts": ["api.example.com"]
}

Declara solo lo que el plugin realmente necesita. Las declaraciones de capabilities también son lo que el marketplace muestra a los operadores del sitio en el diálogo de consentimiento — las capabilities adicionales son fricción en el momento de la instalación y una señal de seguridad en las auditorías.

Referencia de capabilities

CapabilityOtorga acceso a
content:readctx.content.get(), ctx.content.list()
content:writectx.content.create(), ctx.content.update(), ctx.content.delete() (implica content:read)
media:readctx.media.get(), ctx.media.list()
media:writectx.media.getUploadUrl(), ctx.media.upload(), ctx.media.delete() (implica media:read)
network:requestctx.http.fetch() — restringido a allowedHosts
network:request:unrestrictedctx.http.fetch() sin restricción de host (solo para URLs configuradas por el usuario)
users:readctx.users.get(), ctx.users.getByEmail(), ctx.users.list()
email:sendctx.email.send() (requiere un plugin proveedor de email configurado)
hooks.email-transport:registerPermite registrar el hook exclusivo email:deliver (proveedores de transporte)
hooks.email-events:registerPermite registrar hooks email:beforeSend / email:afterSend
hooks.page-fragments:registerPermite registrar el hook page:fragments (solo plugins nativos)

Algunas cosas que vale la pena saber:

  • Implicaciones. content:write implica automáticamente content:read; media:write implica media:read; network:request:unrestricted implica network:request. No necesitas listar ambas.
  • network:request:unrestricted existe para URLs configuradas por el usuario. Un plugin de webhook donde el operador escribe la URL de destino necesita alcanzar hosts que no están en el manifest. Los plugins que siempre llaman a APIs conocidas deben usar network:request + allowedHosts.
  • email:send está controlado por configuración, no solo por la capability. Un plugin puede declarar email:send, pero ctx.email solo se poblará si algún otro plugin ha registrado un transporte email:deliver.

Listas de hosts de red permitidos

Los plugins con network:request solo pueden obtener hosts listados en allowedHosts. Se admiten comodines para subdominios:

"capabilities": ["network:request"],
"allowedHosts": [
	"api.example.com",     // host exacto
	"*.cdn.example.com"    // cualquier subdominio de cdn.example.com
]

El puente verifica el host de la URL de la solicitud contra la lista permitida antes de reenviar la solicitud. Una solicitud a un host que no fue declarado lanza un error dentro del plugin sin salir nunca de la sandbox.

network:request:unrestricted omite completamente la verificación de lista permitida. Está destinado a plugins donde el operador configura la URL de destino en tiempo de ejecución (enviadores de webhook, reenviadores HTTP genéricos). Evítalo para plugins donde el destino es parte del diseño del plugin — declara network:request con hosts explícitos en su lugar, para que el diálogo de consentimiento indique a los operadores exactamente a dónde va a llamar el plugin.

Lo que la sandbox hace cumplir

Cuando un ejecutor de sandbox está activo, el runtime hace cumplir:

  1. Restricción de capabilities. La fábrica PluginContext solo llena ctx.content, ctx.media, ctx.http, ctx.users, ctx.email cuando se declara la capability correspondiente. Llamar a un método en una capability no declarada no es posible — no hay objeto allí.

  2. Ámbito de storage y KV. Cada operación de storage y KV está limitada al slug del plugin. Un plugin no puede leer el KV o las colecciones de storage de otro plugin, y solo puede acceder a las colecciones de storage que declaró en el manifest.

  3. Aislamiento de red. El fetch() directo y otros primitivos de red son bloqueados por el ejecutor. La única forma de alcanzar la red es ctx.http.fetch(), que pasa por la validación de host del puente.

  4. Sin enlaces de host. Los plugins en sandbox no ven variables de entorno, el sistema de archivos ni ningún enlace de plataforma — incluso si tu worker host los tiene. El runtime del plugin es un aislado limpio con solo el puente y las capabilities declaradas.

  5. Límites de recursos. El ejecutor puede hacer cumplir límites de CPU, subrequest, wall-clock y memoria por invocación. Los límites exactos dependen del ejecutor que estés usando; el ejecutor de Cloudflare usa los límites del Worker Loader de la plataforma (50ms CPU por invocación, 10 subrequests, 30 segundos wall-clock, ~128MB memoria). Los hooks que exceden los límites del ejecutor se abortan; el timeout de hook de EmDash (timeout en la configuración del hook) impone un techo más estricto además de eso.

Lo que la sandbox no hace cumplir

Algunas cosas que el sistema de capabilities no cubre y no puede cubrir:

  • Comportamiento dentro de una capability otorgada. Un plugin con content:write puede editar cualquier contenido, no solo el suyo. Las capabilities son gruesas — dicen “este plugin puede escribir contenido”, no “este plugin solo puede escribir el contenido que creó”. La revisión en tiempo de auditoría es la única verificación sobre lo que un plugin realmente hace dentro de su concesión.
  • Confianza del operador en Node.js. Cuando el ejecutor de sandbox configurado informa que no está disponible (sin Cloudflare Worker Loader, sin ejecutor del lado de Node instalado, etc.), los plugins sandboxed: [] se omiten al inicio. Puedes moverlos a plugins: [] para ejecutarlos en proceso — pero entonces no hay aislado V8, sin límites de recursos, y el plugin puede llamar a fetch() directamente o leer variables de entorno. Trata eso como confianza a nivel nativo.
  • Canales laterales. El timing, la salida de log y los datos almacenados son todos visibles para cualquier persona con acceso apropiado al entorno host. No uses la sandbox como un límite de confidencialidad contra el operador que la ejecuta.

Consentimiento de capabilities

Cuando un operador instala un plugin en sandbox desde el marketplace, EmDash muestra un diálogo de consentimiento enumerando las capabilities declaradas. Las actualizaciones que agregan capabilities — por ejemplo, un plugin que anteriormente solo leía contenido ahora quiere hacer solicitudes de red — aparecen como una diferencia de capabilities y requieren aprobación nueva antes de que la nueva versión surta efecto.

Por eso declarar capabilities adicionales importa incluso si “podrías usarlas más tarde”. Aparecen como fricción en cada instalación y actualización, y las auditorías de seguridad marcan plugins que piden más de lo que obviamente necesitan. Lista exactamente lo que el plugin usa, y agrega nuevas capabilities en una versión real cuando el plugin realmente comienza a usarlas.

Validación en tiempo de bundle

emdash-plugin bundle y emdash-plugin publish realizan verificaciones adicionales:

  • Cada capability declarada debe estar en el conjunto reconocido (los errores tipográficos fallan la construcción).
  • network:request requiere un allowedHosts no vacío; network:request:unrestricted requiere que esté vacío. Ver la referencia del manifest.
  • El backend.js empaquetado no puede importar built-ins de Node.js (fs, path, child_process, etc.) — los runtimes de sandbox no los proporcionan.

Ver Bundling and publishing para la lista completa de verificaciones.