플러그인 샌드박스

이 페이지

EmDash는 플러그인을 trustedsandboxed 두 가지 실행 모드로 돌릴 수 있습니다. 이 페이지에서는 각 모드의 동작, 제공하는 보호, 배포 대상별 보안 영향을 설명합니다.

실행 모드

TrustedSandboxed
실행 위치메인 프로세스격리된 V8 isolate(Dynamic Worker Loader)
Capabilities참고용(강제되지 않음)런타임에 강제
리소스 제한없음CPU, 메모리, 하위 요청, 경과 시간
네트워크제한 없음차단됨. ctx.http와 호스트 허용 목록만
데이터 접근DB 전체 접근선언한 capabilities를 통한 RPC 브리지로 제한
사용 가능모든 플랫폼Cloudflare Workers만

Trusted 모드

Trusted 플러그인은 Astro 사이트와 같은 프로세스에서 실행됩니다. npm 패키지나 로컬 파일에서 로드되며 astro.config.mjs에서 설정합니다.

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

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

Trusted 모드에서:

  • Capabilities는 문서일 뿐 강제되지 않습니다. ["read:content"]를 선언해도 프로세스 내 모든 것에 접근할 수 있습니다. capabilities 필드는 관리자에게 플러그인이 의도하는 사용 범위를 알려 줍니다.
  • 리소스 제한이 없습니다. CPU, 메모리, 네트워크 사용량에 상한이 없어 문제 있는 플러그인이 전체 요청을 멈출 수 있습니다.
  • 프로세스 전체 접근. 플러그인은 Astro 사이트와 동일한 Node.js/Workers 런타임을 공유합니다. 임의 모듈 import, 환경 변수 접근, (Node.js에서는) 파일 시스템 읽기/쓰기가 가능합니다.

Sandboxed 모드(Cloudflare Workers)

Sandboxed 플러그인은 Cloudflare의 Dynamic Worker Loader API가 제공하는 격리 V8 isolate에서 실행됩니다. 각 플러그인은 제한이 적용된 자체 isolate를 갖습니다.

샌드박싱을 켜려면 Astro 설정에서 sandbox runner를 구성합니다.

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

샌드박스가 강제하는 것

  1. Capability 강제

    플러그인이 capabilities: ["read:content"]를 선언하면 ctx.content.get()ctx.content.list()만 호출할 수 있습니다. ctx.content.create()를 시도하면 권한 오류가 납니다. RPC 브리지가 강제하며, DB에 직접 접근할 수 없어 우회할 수 없습니다.

  2. 리소스 제한

    각 호출(훅 또는 라우트)은 다음과 함께 실행됩니다.

    리소스기본값강제 주체
    CPU 시간50msWorker Loader(V8 isolate)
    하위 요청호출당 10Worker Loader(V8 isolate)
    경과 시간30초EmDash 러너(Promise.race)
    메모리~128MBV8 플랫폼 상한(플러그인별 설정 불가)

    CPU 또는 하위 요청 한도 초과 시 Worker Loader가 isolate를 중단하고 예외를 던집니다. 경과 시간 초과 시 EmDash가 호출 Promise를 거부합니다. 메모리는 V8 상한으로 제한되지만 플러그인별로는 설정할 수 없습니다.

    위는 내장 기본값입니다. SandboxRunnerFactorySandboxOptions.limits에 다른 값을 넘기면 사용자 정의 제한을 줄 수 있습니다. EmDash 통합 설정을 통한 사이트별 구성은 아직 없습니다.

  3. 네트워크 격리

    Sandboxed 플러그인은 globalOutbound: null이어서 직접 fetch() 호출이 V8 수준에서 차단됩니다. ctx.http.fetch()를 사용해야 하며 브리지를 통해 프록시됩니다. 브리지는 대상 호스트를 플러그인의 allowedHosts와 대조합니다.

  4. 스토리지 범위

    KV, 컬렉션 등 모든 스토리지 작업은 플러그인 ID로 범위가 제한됩니다. 다른 플러그인 데이터는 읽을 수 없습니다. 콘텐츠·미디어 접근은 브리지를 거치며 호출마다 capability를 검사합니다.

  5. 기능 제한

    다음은 Trusted 모드에서만 사용할 수 있습니다.

    • API routes — 사용자 정의 REST 엔드포인트(routes)는 사용할 수 없습니다. Sandboxed 플러그인은 Block Kit 관리 페이지와 훅으로 상호작용합니다.
    • Portable Text 블록 타입 — PT 블록은 사이트 측 렌더링용 Astro 컴포넌트(componentsEntry)가 필요하며 빌드 시 npm에서 로드됩니다. Sandboxed 플러그인은 런타임에 설치되어 컴포넌트를 포함할 수 없습니다.
    • 사용자 정의 React 관리 페이지 — Sandboxed 플러그인은 React 컴포넌트 대신 Block Kit로 관리 UI를 구성합니다.

    플러그인이 이런 기능을 선언하면 emdash plugin bundle이 경고합니다.

아키텍처

Sandboxed 플러그인은 RPC 브리지로 EmDash와 통신합니다.

┌─────────────────────┐     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     │
                                     └──────────────┘

플러그인 코드는 V8 isolate에서 실행되며, ctx의 각 메서드는 브리지로의 프록시입니다. 브리지는 메인 EmDash worker에서 동작하며 capability 검증 후 실제 DB/스토리지 작업을 수행합니다.

Wrangler 구성

샌드박싱에는 Dynamic Worker Loader가 필요합니다. wrangler.jsonc에 추가하세요.

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

Node.js 배포

Node.js(또는 Cloudflare가 아닌 플랫폼)에 배포할 때:

  • NoopSandboxRunner가 사용되며 isAvailable() === false를 반환합니다.
  • Sandboxed 플러그인 로드를 시도하면 SandboxNotAvailableError가 발생합니다.
  • 모든 플러그인은 plugins 배열에 Trusted로 등록해야 합니다.
  • capability 선언은 정보용일 뿐 강제되지 않습니다.

보안상 의미

위협Cloudflare(Sandboxed)Node.js(Trusted만)
허용되지 않은 데이터 읽기브리지 capability 검사로 차단막지 못함 — DB 전체 접근
무단 네트워크 호출globalOutbound: null + 호스트 허용으로 차단막지 못함 — 직접 fetch() 가능
CPU 고갈Worker Loader가 isolate 중단막지 못함 — 이벤트 루프 블로킹
메모리 고갈Worker Loader가 isolate 종료막지 못함 — 프로세스 크래시 가능
환경 변수 접근없음(격리 V8 컨텍스트)막지 못함process.env 공유
파일 시스템 접근Workers에는 FS 없음막지 못함fs 전체 접근

Node.js 배포 권장 사항

  1. 신뢰할 수 있는 출처의 플러그인만 설치. 설치 전 소스 코드를 검토하고 알려진 유지 관리자의 플러그인을 선호하세요.
  2. Capability를 리뷰 체크리스트로 사용. 강제되지 않아도 의도된 범위를 문서화합니다. 네트워크가 필요 없는데 ["network:fetch"]를 선언한 것은 수상합니다.
  3. 리소스 사용을 모니터링. 프로세스 수준 모니터링(예: --max-old-space-size, 헬스 체크)으로 비정상 플러그인을 잡으세요.
  4. 신뢰할 수 없는 플러그인은 Cloudflare 고려. 출처를 알 수 없는 플러그인(마켓플레이스 등)을 실행해야 한다면 샌드박스가 있는 Cloudflare Workers에 배포하세요.

API는 같고 보장은 다름

실행 모드와 관계없이 플러그인 코드는 동일합니다. definePlugin() API, ctx 형태, hooks, routes, storage 사용은 같고, 달라지는 것은 강제 여부입니다.

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

로컬에서는 Trusted로 빠르게 개발·디버깅하고, 프로덕션에서는 코드 변경 없이 Sandboxed로 배포하는 것이 목표입니다.