# How Are Plugins Sandboxed in Instatic? QuickJS-WASM Isolation and Permission Bridges Explained

> Learn how Instatic sandboxes plugins using QuickJS-WASM for runtime isolation and exploits a permission bridge for secure API access. Discover our robust security model.

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

---

**Instatic isolates every plugin inside a QuickJS-WASM runtime that deliberately exposes no Node or Bun APIs, enforcing security through build-time static analysis, runtime memory isolation, and a permission-governed SDK bridge.**

The CoreBunch/Instatic repository implements a defense-in-depth strategy to ensure untrusted plugin code cannot access the host server. Every plugin’s server entrypoint and module pack execute inside a WebAssembly-based JavaScript interpreter, with strict controls that prevent filesystem access, process spawning, or unauthorized network requests.

## The Three-Layer Sandbox Architecture

Instatic enforces plugin isolation through three distinct security layers that operate at different stages of the plugin lifecycle.

### Build-Time Static Analysis

Before a plugin ever reaches a live server, the CLI scans bundled source code for forbidden literals that would indicate attempts to escape the sandbox. This validation lives in [`src/core/plugins/sandboxScan.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/sandboxScan.ts) and runs during both local builds (`instatic-plugin build`) and server-side uploads.

The scanner rejects any bundle containing references to Node or Bun internals:

```typescript
// src/core/plugins/sandboxScan.ts
const FORBIDDEN_SANDBOX_LITERALS = [
  "'node:",
  '"node:',
  "'bun:",
  '"bun:',
  'require(',
  'process.binding',
  'globalThis.process.env',
] as const;

export function assertSandboxSafe(source: string, sourceLabel: string): void {
  const findings = findSandboxLiterals(source);
  if (findings.length) {
    const offenders = findings.map(f => f.literal).join(', ');
    throw new Error(
      `Plugin sandbox: bundle for "${sourceLabel}" references forbidden literals: ${offenders}.\n` +
      `Plugins run inside a QuickJS-WASM sandbox with no access to Node/Bun runtime APIs.`
    );
  }
}

```

If a developer accidentally includes `import 'node:fs'` or `process.binding`, the build aborts immediately with a descriptive error, preventing unsafe code from reaching the runtime environment.

### Runtime QuickJS VM Isolation

When the server activates a plugin, it creates a dedicated QuickJS context that operates as a true memory sandbox. The `SandboxesModulePack` interface in [`src/core/plugins/modulePackLoader.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/modulePackLoader.ts) defines the contract for these isolated containers:

```typescript
// src/core/plugins/modulePackLoader.ts
export interface SandboxedModulePack {
  readonly pluginId: string;
  render(moduleId:string, props:Record<string,unknown>, children:string[]): {html:string; css?:string; js?:string};
  preview(...): any;
  dispose(): void;               // frees the QuickJS native context
}

```

The `activateSandboxedPluginModulePack` function instantiates the VM only after verifying the plugin manifest includes the `modules.register` permission. Each render call executes inside the WebAssembly boundary, ensuring that crashes, infinite loops, or memory exhaustion within the plugin cannot affect the host Node.js or Bun process.

### Permission-Driven SDK Bridges

While the QuickJS sandbox blocks direct system access, plugins still require limited capabilities like HTTP requests or data storage. Instatic exposes these through a strictly controlled SDK bridge defined in [`src/core/plugin-sdk/types/permissions.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/permissions.ts).

For example, outbound network access requires explicit manifest declarations:

```typescript
// src/core/plugin-sdk/types/permissions.ts
// NetworkPermission is granted only when the manifest lists networkAllowedHosts
// and the bridge validates the URL against that allow-list.

```

When sandboxed code invokes `api.cms.fetch(url, opts)`, the bridge performs two validation checks:

1. The plugin manifest includes `network` in `grantedPermissions`.
2. The target host matches an entry in `networkAllowedHosts`.

If either check fails, the bridge throws a sandbox-level `Error` that the plugin may catch but cannot bypass. This **capability-based security model** ensures plugins access only the specific resources declared in their manifest.

## Build-Time Scanning: Blocking Unsafe Code Before It Runs

The `assertSandboxSafe` function serves as the first line of defense against supply-chain attacks and developer errors. By scanning for literal strings like `require(`, `node:`, and `bun:`, the scanner detects attempts to import native modules before the code ships.

This static analysis runs in two contexts:

- **Development**: The CLI plugin build command fails fast when detecting forbidden patterns.
- **Production**: The server re-scans uploaded ZIP packages during installation, providing defense in depth against tampered bundles.

## Runtime Isolation: QuickJS-WASM Memory Safety

Unlike Node.js VM contexts or worker threads, the QuickJS-WASM sandbox shares no memory space with the host process. The `SandboxesModulePack` created by [`modulePackLoader.ts`](https://github.com/CoreBunch/Instatic/blob/main/modulePackLoader.ts) wraps the QuickJS interpreter in a WebAssembly boundary that implements only a subset of standard Web `Request`/`Response` APIs required for plugin I/O.

Key safety characteristics of this architecture:

- **Memory isolation**: The WASM heap is disjoint from the host's JavaScript heap, preventing prototype pollution or buffer overflow attacks from escaping.
- **API restriction**: The global scope within the sandbox contains no `process`, `Buffer`, `require`, or filesystem utilities.
- **Resource cleanup**: The `dispose()` method frees the native QuickJS context when a plugin deactivates or upgrades, preventing memory leaks and stale state accumulation.

## Permission Governance: Controlled Host Access

The bridge architecture in [`src/core/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts) exposes host capabilities through namespaced APIs like `api.store`, `api.dashboardWidget`, and `api.cms.routes`. Each API group checks against the permission manifest before executing host-side code.

The `network` permission demonstrates this pattern:

- **Declaration**: The plugin manifest must list `network` in `grantedPermissions` and specify allowed hosts in `networkAllowedHosts`.
- **Validation**: The bridge validates URLs against the allow-list before initiating the fetch.
- **Enforcement**: Attempts to contact non-approved hosts throw immediate errors visible only within the sandbox.

This model extends to other sensitive operations, ensuring plugins follow the **principle of least privilege** by default.

## Sandbox Lifecycle: From Build to Teardown

Instatic applies sandboxing consistently across the entire plugin lifecycle:

1. **Authoring**: Developers write standard JavaScript/TypeScript without special syntax constraints.
2. **Build**: The bundler produces the entrypoint, then `assertSandboxSafe` validates the output against the forbidden literals list.
3. **Upload**: The server repeats the static analysis when accepting the plugin package.
4. **Activation**: The host creates a fresh QuickJS-WASM VM via `activateSandboxedPluginModulePack`, registering module metadata while keeping execution inside the sandbox.
5. **Execution**: All `render` calls execute within the VM; only approved SDK functions bridge to the host.
6. **Teardown**: `dispose()` frees native resources when the plugin disables or the server shuts down.

## Summary

- **QuickJS-WASM isolation** ensures plugins run in a memory-safe WebAssembly interpreter with no access to Node/Bun APIs, as implemented in [`src/core/plugins/modulePackLoader.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/modulePackLoader.ts).
- **Build-time scanning** via [`src/core/plugins/sandboxScan.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/sandboxScan.ts) blocks forbidden literals like `node:` imports and `process.binding` before code ever reaches a server.
- **Permission bridges** defined in [`src/core/plugin-sdk/types/permissions.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/permissions.ts) grant capabilities like network access only when explicitly declared in the plugin manifest and validated against allow-lists.
- **Automatic cleanup** through the `dispose()` method on `SandboxedModulePack` prevents resource leaks when plugins deactivate or upgrade.

## Frequently Asked Questions

### Can plugins access the filesystem or spawn processes?

No. The QuickJS-WASM sandbox in Instatic exposes no filesystem or process APIs. According to the source code in [`src/core/plugins/sandboxScan.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/sandboxScan.ts), any attempt to include `require(`, `node:fs`, `bun:`, or `process.binding` triggers a build failure. Runtime execution occurs entirely within a WebAssembly boundary that lacks these capabilities.

### What happens if a plugin crashes or enters an infinite loop?

The isolated QuickJS context contains crashes within the WebAssembly sandbox. Because the VM shares no memory with the host process, exceptions thrown inside `render` calls bubble up only to the bridge layer, which logs the error without affecting other plugins or the core server. The host can terminate individual plugin VMs via the `dispose()` method without restarting the entire application.

### How does the network permission system work?

Network access requires explicit opt-in through the plugin manifest. As defined in [`src/core/plugin-sdk/types/permissions.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/permissions.ts), the plugin must declare `network` in `grantedPermissions` and list approved domains in `networkAllowedHosts`. When sandboxed code calls `api.cms.fetch()`, the bridge validates the URL against this allow-list before executing the request, rejecting any attempts to contact unauthorized hosts.

### Is the sandbox vulnerability-free against malicious code?

While no security system is absolute, Instatic's three-layer approach—static analysis, WASM memory isolation, and capability-based permissions—significantly reduces the attack surface. The QuickJS interpreter provides a formally smaller trusted computing base than a full Node.js VM, and the removal of Node-specific globals prevents common escape techniques. However, administrators should still only install plugins from trusted sources and review requested permissions carefully.