Capacidades y seguridad

En esta página

Los plugins sandboxed están aislados por defecto. Para hacer algo más allá de leer y escribir su propio KV y almacenamiento, un plugin debe declarar una capacidad en su manifiesto. El puente de la sandbox controla cada API proporcionada por el host basándose en estas declaraciones — un plugin que no declaró content:read no obtiene ctx.content, y uno que no declaró network:request no obtiene ctx.http.

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

Declarar capacidades

Las capacidades se encuentran en emdash-plugin.jsonc, junto con slug y el resto del contrato de confianza:

{
	"slug": "plugin-hello",
	// ...identidad + perfil...

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

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

Referencia de capacidades

CapacidadOtorga acceso a
content:readctx.content.get(), ctx.content.list()
content:writectx.content.create(), ctx.content.update(), ctx.content.delete() (implica content:read)
taxonomies:readctx.taxonomies.getAll(), ctx.taxonomies.getTerms(), ctx.taxonomies.getEntryTerms()
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 (proveedor 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 a tener en cuenta:

  • Implicaciones. content:write implica automáticamente content:read; media:write implica media:read; network:request:unrestricted implica network:request. No necesitas listar ambos.
  • Las taxonomías son una superficie separada de solo lectura. taxonomies:read otorga acceso a las definiciones de taxonomías, sus términos y los términos asignados a una entrada a través de ctx.taxonomies. Es independiente de content:read — declara ambos si el plugin lee contenido y su clasificación. No hay acceso de escritura a taxonomías desde plugins.
  • network:request:unrestricted existe para URLs configuradas por el usuario. Un plugin de webhook donde el operador ingresa la URL de destino necesita alcanzar hosts que no están en el manifiesto. Los plugins que siempre llaman a APIs conocidas deberían usar network:request + allowedHosts.
  • email:send está controlado por configuración, no solo por la capacidad. Un plugin puede declarar email:send, pero ctx.email solo se completa si otro plugin ha registrado un transporte email:deliver.

Listas de hosts permitidos para red

Los plugins con network:request solo pueden hacer fetch a 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 de permitidos antes de reenviar la solicitud. Una solicitud a un host no declarado lanza una excepción dentro del plugin sin salir nunca de la sandbox.

network:request:unrestricted omite completamente la verificación de la lista de permitidos. Está destinado a plugins donde el operador configura la URL de destino en tiempo de ejecución (emisores 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 diga a los operadores exactamente a dónde llamará el plugin.

Lo que la sandbox aplica

Cuando un runner de sandbox está activo, el runtime aplica:

  1. Control por capacidades. La fábrica de PluginContext solo completa ctx.content, ctx.taxonomies, ctx.media, ctx.http, ctx.users, ctx.email si la capacidad correspondiente está declarada. Llamar a un método en una capacidad no declarada no es posible — no hay objeto allí.

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

  3. Aislamiento de red. fetch() directo y otras primitivas de red son bloqueadas por el runner. El único camino hacia la red es ctx.http.fetch(), que pasa por la validación de host del puente.

  4. Sin bindings del host. Los plugins sandboxed no ven variables de entorno, sistema de archivos ni bindings de plataforma — incluso si tu worker host los tiene. El runtime del plugin es un aislado limpio con solo el puente y las capacidades declaradas.

  5. Límites de recursos. El runner puede aplicar límites de CPU, sub-solicitudes, tiempo de reloj y memoria por invocación. Los límites exactos dependen de qué runner estés usando; el runner de Cloudflare usa los límites del Worker Loader de la plataforma (50ms CPU por invocación, 10 sub-solicitudes, 30 segundos de tiempo de reloj, ~128MB de memoria). El runner workerd de Node.js (@emdash-cms/sandbox-workerd) aplica tiempo de reloj vía Promise.race; los límites de CPU y memoria son características de la plataforma Cloudflare y no se aplican por workerd independiente. Los hooks que exceden los límites del runner se cancelan; el timeout de hook de EmDash (timeout en la configuración del hook) aplica un límite superior más estricto además.

Lo que la sandbox no aplica

Algunas cosas que el sistema de capacidades no cubre ni puede cubrir:

  • Comportamiento dentro de una capacidad otorgada. Un plugin con content:write puede editar cualquier contenido, no solo el suyo. Las capacidades son de grano grueso — 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 de lo que un plugin realmente hace dentro de su concesión.
  • Confianza del operador en Node.js. Si el runner de sandbox configurado informa que no está disponible (sin Cloudflare Worker Loader, sin runner del lado de Node instalado, etc.), los plugins sandboxed: [] se omiten en el arranque. Puedes moverlos a plugins: [] para ejecutarlos en proceso — pero entonces no hay aislado V8, no hay 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. Temporización, salida de logs y datos almacenados son visibles para cualquier persona con acceso razonable al entorno del host. No uses la sandbox como frontera de confidencialidad contra el operador que la ejecuta.

Consentimiento de capacidades

Cuando un operador instala un plugin sandboxed desde el Marketplace, EmDash muestra un diálogo de consentimiento con las capacidades declaradas. Las actualizaciones que agregan capacidades — por ejemplo, un plugin que antes solo leía contenido y ahora quiere hacer solicitudes de red — se muestran como un diff de capacidades y requieren aprobación nueva antes de que la nueva versión tome efecto.

Por eso es importante declarar capacidades adicionales incluso si “podrías necesitarlas 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 capacidades en una versión real cuando el plugin realmente comience a usarlas.

Validación en tiempo de build

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

  • Cada capacidad declarada debe estar en el conjunto reconocido (los errores tipográficos hacen fallar el build).
  • network:request requiere un allowedHosts no vacío; network:request:unrestricted requiere que esté vacío. Ver la referencia del manifiesto.
  • 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 Empaquetar y publicar para la lista completa de verificaciones.