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

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)【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, 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)【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)【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)【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】.

// 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.

// 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.

// 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, 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →