# Instatic Plugin Security Model and QuickJS-WASM Sandbox: A Deep Dive

> Explore the Instatic plugin security model. Discover how its hardened QuickJS-WASM sandbox isolates plugins, blocks APIs, enforces permissions, and secures secrets.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: deep-dive
- Published: 2026-07-27

---

**Instatic isolates every third-party plugin inside a hardened QuickJS-WASM sandbox that blocks Node.js and Bun APIs, enforces capability-based permissions, and prevents raw bytes and secrets from crossing the security boundary.**

The Instatic platform, maintained in the `CoreBunch/Instatic` repository, implements a **capability-based security model** to safely execute untrusted third-party code. By leveraging a WebAssembly-compiled QuickJS engine, the system creates a strict isolation boundary between the host runtime and plugin logic. This architecture ensures that extensions operate with the minimum necessary privileges while protecting user data and host resources from arbitrary code execution.

## QuickJS-WASM Sandbox Architecture

### Isolated Execution Context

The sandbox instantiation begins in [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts), which spins up a single-instance QuickJS WASM module and creates a fresh `QuickJSContext` for each plugin. This context has **no access to the host process** except for explicitly registered polyfills and bridge functions. Each plugin receives its own isolated VM instance, preventing cross-plugin memory access or state leakage.

The factory function in [`vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/vm.ts) wires host-side bridges to the sandbox only after validating the plugin's declared permissions, ensuring that unauthorized capabilities remain unreachable from within the QuickJS context.

### Static Analysis and Forbidden API Detection

Before any code enters the sandbox, [`src/core/plugins/sandboxScan.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/sandboxScan.ts) performs static analysis on plugin bundles to detect forbidden literals. The scanner blocks any usage of `require()`, `node:fs`, `Bun.spawn`, or other Node.js/Bun-specific APIs. If the build process detects these patterns, the plugin fails validation and cannot be published or loaded.

This static analysis operates as the first line of defense, catching privilege escalation attempts before runtime execution.

## Capability-Based Permission System

### Permission Declarations in Manifests

Plugins declare their required capabilities in [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json), which [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts) validates against the schema defined in [`src/core/plugin-sdk/types/permissions.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/permissions.ts). Each permission maps to a concrete host bridge:

- `network.outbound` → Exposes the sandboxed `fetch()` polyfill
- `cms.routes.public` → Allows registration of public HTTP endpoints
- `cms.media.upload` → Grants access to media storage adapters

The host validates these declarations during sandbox initialization. A plugin cannot access any host functionality it does not explicitly request and the user does not approve.

### Network Access Controls

Outbound HTTP requests are strictly controlled through the `networkAllowedHosts` array in the plugin manifest. When a plugin invokes the sandboxed `fetch()` implementation (provided via [`src/core/plugin-sdk/types/serverApi.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/serverApi.ts)), the host bridge checks the target hostname against this whitelist. Requests to non-whitelisted hosts are rejected at the bridge level, preventing data exfiltration to unauthorized endpoints.

## Resource Constraints and Execution Limits

[`server/plugins/quickjs/limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/limits.ts) imposes hard resource ceilings on every VM instance. Each plugin operates within a confined **memory budget** and **execution-time limit** that prevents runaway code from consuming host resources. These constraints apply to all sandboxed entry points, including cron-style loops defined via [`src/core/plugin-sdk/capabilities.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/capabilities.ts).

If a plugin exceeds its allocated CPU time or memory allocation, the QuickJS runtime terminates the context immediately, isolating the failure to the specific plugin without affecting the host process or other extensions.

## Data Flow Security Boundaries

### Media Storage Isolation

The platform enforces a strict "bytes never cross the sandbox" policy, as documented in [`mediaStorageRegistry.ts`](https://github.com/CoreBunch/Instatic/blob/main/mediaStorageRegistry.ts). When handling file uploads or storage operations, the sandboxed plugin receives only **signed URLs** or metadata, never raw byte streams. The host process manages all binary data transfer directly with storage backends, while the plugin manipulates only references and configuration objects.

This design prevents malicious plugins from intercepting or exfiltrating user media data during upload or download operations.

### Deterministic Bridge Communication

All communication between the host and VM traverses [`server/plugins/quickjs/marshal.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/marshal.ts), which enforces **JSON-serializable data** constraints. The marshaller rejects exotic objects, functions, or circular references, ensuring that only pure data crosses the security boundary. This prevents prototype pollution attacks and maintains deterministic behavior across the bridge.

## Secret Management and Injection

Sensitive credentials never enter the sandbox. [`server/repositories/pluginSecrets.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/pluginSecrets.ts) manages secret storage and injection on the host side exclusively. When a plugin requires a secret value (such as an API key), the host resolves the reference and injects the value into the request at the bridge level. The sandbox receives only the final computed result, maintaining a "never leak secrets" invariant that protects against memory scraping and logging attacks.

## Unsandboxed Entry Points

While server-side plugin code runs exclusively within the QuickJS-WASM sandbox, [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts) recognizes two exceptions: `entrypoints.editor` and `entrypoints.app`. These entry points run **unsandboxed** within the admin window context, possessing full browser privileges. The manifest validator enforces strict trust requirements on plugins declaring these entry points, as they bypass the WASM isolation boundary.

## Practical Implementation Examples

### Declaring Sandbox Permissions

```json
{
  "id": "my.sample",
  "version": "0.1.0",
  "entrypoints": {
    "server": "./dist/server/index.js"
  },
  "permissions": [
    "network.outbound",
    "cms.routes.public",
    "cms.media.upload"
  ],
  "networkAllowedHosts": ["api.example.com"]
}

```

The `permissions` array drives the host-side bridge initialization in [`src/core/plugin-sdk/types/permissions.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/permissions.ts), determining which capabilities the sandbox can access.

### Using the Sandboxed Fetch API

```typescript
import { fetch } from '@core/plugin-sdk';

export async function handler() {
  // Executes within QuickJS sandbox, respecting networkAllowedHosts whitelist
  const res = await fetch('https://api.example.com/data');
  return res.json();
}

```

### Registering a Media Adapter

```typescript
import { registerStorageAdapter } from '@core/plugin-sdk/media';

registerStorageAdapter({
  id: 's3',
  servingMode: 'signedUrl',
  getUploadPlan: async (path) => ({
    url: `https://s3.amazonaws.com/${path}?signature=...`,
    method: 'PUT',
    headers: { 'Content-Type': 'application/octet-stream' }
  })
});

```

The adapter executes on the host side; the sandbox receives only the signed URL configuration, never the raw bytes.

### Defining a Resource-Limited Cron Loop

```typescript
import { defineLoop } from '@core/plugin-sdk/capabilities';

defineLoop({
  schedule: '0 */5 * * *',
  handler: async () => {
    // Runs inside QuickJS VM with memory/CPU limits from limits.ts
    await performPeriodicTask();
  }
});

```

## Summary

- **QuickJS-WASM Isolation**: Each plugin runs in a dedicated `QuickJSContext` created by [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts), with no direct access to Node.js or Bun APIs.
- **Static Prohibition**: [`src/core/plugins/sandboxScan.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/sandboxScan.ts) blocks forbidden literals like `require()` and `node:fs` during the build phase.
- **Capability-Based Access**: Plugins declare permissions in [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) that map to specific host bridges defined in [`src/core/plugin-sdk/types/permissions.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/permissions.ts).
- **Resource Enforcement**: Hard memory and execution-time limits in [`server/plugins/quickjs/limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/limits.ts) prevent resource exhaustion attacks.
- **Data Boundary Protection**: Raw bytes never cross the sandbox boundary; media operations use signed URLs handled by [`mediaStorageRegistry.ts`](https://github.com/CoreBunch/Instatic/blob/main/mediaStorageRegistry.ts).
- **Secret Isolation**: [`server/repositories/pluginSecrets.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/pluginSecrets.ts) keeps credentials on the host side, injecting values only at the bridge level.
- **Typed Marshalling**: [`server/plugins/quickjs/marshal.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/marshal.ts) restricts bridge communication to JSON-serializable data, preventing object injection attacks.

## Frequently Asked Questions

### How does Instatic prevent plugins from accessing the filesystem directly?

The platform employs a two-layer defense. First, [`src/core/plugins/sandboxScan.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/sandboxScan.ts) statically analyzes bundles for forbidden patterns including `node:fs`, `fs/promises`, or `require()` calls. Second, the QuickJS-WASM runtime in [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts) executes without host bindings to filesystem APIs, ensuring that even if static analysis misses an edge case, the runtime environment lacks the capability to perform file operations.

### What happens when a plugin exceeds its memory or CPU limits?

[`server/plugins/quickjs/limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/limits.ts) configures hard ceilings for each VM instance. If a plugin consumes its entire memory allocation or exceeds the execution-time budget (such as an infinite loop in a cron handler), the QuickJS runtime immediately terminates that specific context. The host process logs the violation and disables the offending plugin without affecting other extensions or the core platform.

### Can sandboxed plugins make HTTP requests to external APIs?

Yes, but only through controlled channels. Plugins must declare the `network.outbound` permission and list specific hostnames in `networkAllowedHosts`. The `fetch()` implementation exposed to the sandbox validates every request URL against this whitelist at the host bridge level. Requests to non-whitelisted domains are rejected before leaving the host environment, preventing unauthorized data exfiltration.

### Are plugin entry points ever allowed to run outside the sandbox?

Only `entrypoints.editor` and `entrypoints.app` run unsandboxed, executing within the admin window's browser context with full privileges. According to [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts), these entry points undergo stricter trust validation during manifest parsing. All `entrypoints.server` code executes exclusively within the QuickJS-WASM sandbox, regardless of the plugin's trust status.