Sandbox de plugins

Nesta página

O EmDash pode executar plugins em dois modos: trusted e sandboxed. Esta página explica como cada modo funciona, que proteções oferece e as implicações de segurança para diferentes alvos de deployment.

Modos de execução

TrustedSandboxed
Executa emProcesso principalIsolate V8 isolado (Dynamic Worker Loader)
CapabilitiesInformativas (não aplicadas)Aplicadas em tempo de execução
Limites de recursosNenhumCPU, memória, subpedidos, tempo de relógio
Acesso à redeIlimitadoBloqueado; apenas via ctx.http com lista de hosts permitidos
Acesso a dadosAcesso completo à base de dadosLimitado às capabilities declaradas via bridge RPC
Disponível emTodas as plataformasApenas Cloudflare Workers

Modo trusted

Plugins trusted executam no mesmo processo que o site Astro. São carregados a partir de pacotes npm ou ficheiros locais e configurados em astro.config.mjs:

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

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

Em modo trusted:

  • As capabilities são documentação, não enforcement. Um plugin que declara ["read:content"] ainda pode aceder a tudo no processo. O campo capabilities informa os administradores sobre o que o plugin pretende usar.
  • Sem limites de recursos. CPU, memória e rede não são limitados. Um plugin com problemas pode bloquear todo o pedido.
  • Acesso total ao processo. Os plugins partilham o runtime Node.js/Workers com o site Astro. Podem importar qualquer módulo, aceder a variáveis de ambiente e ler/escrever o sistema de ficheiros (em Node.js).

Modo sandboxed (Cloudflare Workers)

Plugins sandboxed executam em isolates V8 isolados fornecidos pela API Dynamic Worker Loader da Cloudflare. Cada plugin tem o seu próprio isolate com limites aplicados.

Para ativar o sandboxing, configure o sandbox runner na config Astro:

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

O que o sandbox impõe

  1. Aplicação de capabilities

    Se um plugin declara capabilities: ["read:content"], só pode chamar ctx.content.get() e ctx.content.list(). Tentar ctx.content.create() lança erro de permissão. Isto é aplicado pelo bridge RPC — o plugin não contorna porque não tem acesso direto à base de dados.

  2. Limites de recursos

    Cada invocação (hook ou rota) corre com:

    RecursoPredefiniçãoAplicado por
    Tempo de CPU50msWorker Loader (isolate V8)
    Subpedidos10 por invocaçãoWorker Loader (isolate V8)
    Tempo de relógio30 segundosRunner EmDash (Promise.race)
    Memória~128MBTeto da plataforma V8 (não configurável por plugin)

    Exceder CPU ou subpedidos faz o Worker Loader abortar o isolate e lançar exceção. Exceder o tempo de relógio faz o EmDash rejeitar a promise de invocação. A memória é limitada pelo teto V8 mas não é configurável por plugin.

    Estes são os valores predefinidos integrados. Limites personalizados podem ser configurados com uma SandboxRunnerFactory que passe outros valores via SandboxOptions.limits. Configuração por site na integração EmDash ainda não está implementada.

  3. Isolamento de rede

    Plugins sandboxed têm globalOutbound: null — chamadas diretas a fetch() são bloqueadas ao nível V8. Devem usar ctx.http.fetch(), proxificado pelo bridge. O bridge valida o host de destino contra a lista allowedHosts do plugin.

  4. Âmbito do armazenamento

    Todas as operações de armazenamento (KV, collections) ficam limitadas ao ID do plugin. Um plugin não lê dados de outro. Acesso a conteúdo e media passa pelo bridge, que verifica capabilities em cada chamada.

  5. Restrições de funcionalidade

    Algumas funcionalidades só existem em modo trusted:

    • API routes — Endpoints REST personalizados (routes) não estão disponíveis. Plugins sandboxed interagem via páginas admin Block Kit e hooks.
    • Tipos de bloco Portable Text — Blocos PT precisam de componentes Astro para render no site (componentsEntry), carregados em build a partir de npm. Plugins sandboxed instalam-se em runtime e não podem incluir componentes.
    • Páginas admin React personalizadas — Plugins sandboxed usam Block Kit para UI admin em vez de componentes React.

    O comando emdash plugin bundle avisa se um plugin declara estas funcionalidades.

Arquitetura

Plugins sandboxed comunicam com o EmDash através de um bridge 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     │
                                     └──────────────┘

O código do plugin corre num isolate V8. Recebe um objeto ctx em que cada método é um proxy para o bridge. O bridge corre no worker principal EmDash e faz as operações reais de base/armazenamento após validar capabilities.

Configuração Wrangler

O sandboxing requer Dynamic Worker Loader. Adicione ao wrangler.jsonc:

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

Deployments Node.js

Ao fazer deploy em Node.js (ou qualquer plataforma que não seja Cloudflare):

  • Usa-se NoopSandboxRunner. Devolve isAvailable() === false.
  • Tentar carregar plugins sandboxed lança SandboxNotAvailableError.
  • Todos os plugins devem ser registados como trusted no array plugins.
  • Declarações de capabilities são apenas informativas — não são aplicadas.

O que isto significa para a segurança

AmeaçaCloudflare (Sandboxed)Node.js (só Trusted)
Plugin lê dados que não deviaBloqueado por verificações de capabilities no bridgeNão impedido — acesso total à BD
Plugin faz chamadas de rede não autorizadasBloqueado por globalOutbound: null + lista de hostsNão impedido — pode chamar fetch() diretamente
Plugin esgota CPUIsolate abortado pelo Worker LoaderNão impedido — bloqueia o event loop
Plugin esgota memóriaIsolate terminado pelo Worker LoaderNão impedido — pode derrubar o processo
Plugin acede a variáveis de ambienteSem acesso (contexto V8 isolado)Não impedido — partilha process.env
Plugin acede ao sistema de ficheirosSem FS em WorkersNão impedido — acesso total a fs

Recomendações para deployments Node.js

  1. Instale plugins apenas de fontes confiáveis. Reveja o código-fonte antes de instalar. Prefira plugins de maintainers conhecidos.
  2. Use capabilities como checklist de revisão. Mesmo sem enforcement documentam o âmbito pretendido. Um plugin com ["network:fetch"] sem necessidade de rede é suspeito.
  3. Monitorize o uso de recursos. Use monitorização ao nível do processo (ex.: --max-old-space-size, health checks) para detetar plugins descontrolados.
  4. Considere Cloudflare para plugins não confiáveis. Se precisar de executar plugins de origem desconhecida (ex.: marketplace), faça deploy em Cloudflare Workers onde o sandboxing está disponível.

Mesma API, garantias diferentes

O código do plugin é idêntico em ambos os modos. A API definePlugin(), a forma de ctx, hooks, routes e storage funcionam da mesma forma. O que muda é o enforcement:

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

O objetivo é permitir que autores desenvolvam localmente em modo trusted (iteração mais rápida, debugging mais simples) e façam deploy em produção em modo sandboxed sem alterar código.