# How Instatic Plugin Sandbox Security Works: Multi-Layer Isolation Explained

> Explore Instatic's plugin sandbox security. Learn how multi-layer isolation using process isolation, WebAssembly VM, resource limits, and permissions protect your system.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-31

---

**Instatic isolates third-party plugins in a multi-layer sandbox that combines process isolation, a restricted WebAssembly VM, strict resource limits, and a fine-grained permission model.**

The CoreBunch/Instatic repository implements a defense-in-depth security architecture designed to safely execute untrusted plugin code without compromising the host server. Understanding how Instatic plugin sandbox security works requires examining the interplay between Bun workers, QuickJS WebAssembly isolation, and runtime permission enforcement.

## Multi-Layer Sandbox Architecture

### Sandbox Topology

The plugin execution environment operates as a stack of isolated layers defined in [[`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts)](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts)【L5-L11】:

1. **Bun host (main process)** – The core server that loads plugins and orchestrates execution.
2. **Bun Worker** – A dedicated worker thread providing crash isolation.
3. **QuickJS-WASM context** – A WebAssembly-compiled QuickJS engine (`quickjs-emscripten`) that runs plugin code with zero direct host access.
4. **Bootstrap + Plugin source** – The SDK façade evaluates first, followed by the compiled plugin bundle as an IIFE populating `globalThis.__plugin_exports`.

This topology ensures that even if one layer fails, the others contain the breach.

### Process Isolation with Bun Workers

Each plugin executes inside a dedicated `Bun.Worker` instance. The worker guarantees **crash isolation**—if a plugin throws an uncaught exception or enters an infinite loop, only the worker terminates, leaving the main server and other plugins unaffected. The worker also provides **CPU yielding**, preventing long-running operations from blocking the host's event loop.

The worker spawns before the QuickJS context instantiates, ensuring any VM failure remains contained within the worker's lifespan.

### WebAssembly VM Isolation

The QuickJS engine runs as a WebAssembly module with strictly limited host function exposure. According to the source in [`vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/vm.ts), the VM only exposes three host functions:

- **`__hostCall`** – Dispatches approved API calls to the host.
- **`__hostSleep`** – Handles asynchronous delays.
- **`__log`** – Provides logging capabilities.

All other system APIs are unavailable to the plugin. The VM runs synchronously (the async variant is deliberately avoided for stability), with calls dispatching through captured bootstrap functions like `__runLifecycle` and `__runRoute`【L24-L31】. These dispatchers are captured immediately after bootstrap evaluation, preventing plugins from hijacking host call mechanisms.

## Resource Limits and Execution Constraints

### Memory and Stack Caps

Instatic enforces hard resource limits defined in [[`server/plugins/quickjs/limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/limits.ts)](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/limits.ts)【L1-L34】:

- **Memory**: **64 MiB** (`DEFAULT_MEMORY_LIMIT_BYTES`)
- **Stack**: **1 MiB** (`DEFAULT_STACK_SIZE_BYTES`)

When the host creates the VM context【L102-L108】, it applies these caps. Allocations exceeding the memory limit trigger an `OutOfMemory` error inside the VM, while the stack limit prevents runaway recursion from exhausting the host's WASM stack.

### Execution Timeouts

The sandbox implements wall-clock deadlines to prevent indefinite execution:

- **General eval timeout**: **5 seconds** (`DEFAULT_EVAL_TIMEOUT_MS`)
- **Module-pack eval timeout**: **2 seconds** (`MODULE_PACK_EVAL_TIMEOUT_MS`)

If a plugin exceeds these limits, the VM aborts the operation and logs an error with the plugin identifier.

## Permission-Based Security Model

### Manifest Declarations

Plugins declare required capabilities in their manifest schema, located in [[`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts)](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts)【L58-L70】. The manifest contains two critical fields:

- **`permissions`** – Capabilities the plugin requests.
- **`grantedPermissions`** – Subset the host actually grants after review.

Available permissions include **`editor.commands`** (palette commands), **`frontend.assets`** (script injection), and **`network.outbound`** (external requests).

### Runtime Permission Enforcement

Every API call routes through `assertPluginPermission` in [[`src/core/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts)](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts)【L74-L78】. This function validates that the required permission exists in the plugin's granted set before executing the operation.

Permission checks occur at every entry point. If a plugin lacks `editor.commands`, the runtime skips command registration and logs a diagnostic【L515-L525】.

```typescript
// Registration fails silently if 'editor.commands' permission is missing
pluginRuntime.registerCommand({
  id: 'my-plugin.say-hello',
  title: 'Say Hello',
  handler: () => console.log('Hello!'),
});

```

## Network Security Controls

### Outbound Allowlist Validation

The **`network.outbound`** permission requires an explicit hostname allowlist in the manifest. When a plugin executes `api.http.fetch`, the host validates the target URL against this allowlist before proceeding. This prevents compromised plugins from contacting arbitrary external services or exfiltrating data to unauthorized domains.

```typescript
// This request only succeeds if 'api.example.com' is in the manifest allowlist
await api.http.fetch('https://api.example.com/data', {
  method: 'GET',
});

```

## Secure Communication and Cleanup

### Data Serialization and Validation

All data crossing the VM boundary serializes as plain strings via `ctx.newString`. The bootstrap's dispatcher functions receive JSON payloads, which the host validates using TypeBox schemas before processing. This serialization layer ensures malformed or malicious data cannot corrupt host state.

### Lifecycle Cleanup and Disposal

When a plugin unloads, the runtime calls `dispose()` on the VM to:

- Clear all host function handles.
- Cancel pending timers.
- Resolve or reject pending deferred promises.
- Release QuickJS handles and the underlying WebAssembly context.

This thorough teardown prevents memory leaks and eliminates references to host objects that could otherwise be exploited.

```typescript
// Internal VM creation example
const vm = await createPluginVm({
  pluginSource: compiledPluginBundle,
  env: { pluginId: 'my-plugin', ... },
});
await vm.__runLifecycle('activate');

```

## Summary

- **Instatic plugin sandbox security** relies on four isolation layers: Bun workers, QuickJS-WASM, resource limits, and permission gating.
- **Resource constraints** enforce 64 MiB memory, 1 MiB stack, and 5-second execution timeouts per plugin.
- **Permission manifests** define granular capabilities (`editor.commands`, `network.outbound`) enforced at runtime via `assertPluginPermission`.
- **Network requests** require explicit hostname allowlists to prevent unauthorized external communication.
- **Cleanup protocols** release all handles and timers when plugins unload, preventing resource leaks.

## Frequently Asked Questions

### What happens if a plugin exceeds its memory or execution limits?

The VM immediately aborts the operation and throws an `OutOfMemory` error or timeout exception. The host logs the error with the plugin identifier while the Bun Worker containing the VM terminates, leaving the main server and other plugins unaffected.

### How does Instatic prevent plugins from accessing unauthorized host APIs?

The runtime enforces a **permission-based gating system**. Plugins must declare required permissions in their manifest, and the host stores granted permissions in `grantedPermissions`. Every API call routes through `assertPluginPermission` in [`runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/runtime.ts), which validates the permission before executing the host function. Without the specific permission, the call returns an error or the resource registration skips silently.

### Can Instatic plugins make network requests to any external server?

No. Plugins require the **`network.outbound`** permission combined with an explicit hostname **allowlist** defined in the manifest. When a plugin calls `api.http.fetch`, the host validates the URL against this allowlist. If the target domain is not pre-approved, the request fails before leaving the sandbox.

### How does the sandbox handle plugin crashes or infinite loops?

Each plugin runs in a dedicated **Bun Worker** that provides **crash isolation**. If a plugin throws an uncaught exception or enters an infinite loop, the worker terminates independently while the main process continues running. Additionally, the **5-second execution timeout** (`DEFAULT_EVAL_TIMEOUT_MS`) interrupts long-running evaluations, preventing hung plugins from consuming CPU indefinitely.