# How Instatic Enforces Plugin Permissions and Network Host Allowlists: A Two-Layer Security Model

> Instatic enforces plugin permissions and network host allowlists using a two-layer security model. Discover how manifest validation and runtime gatekeeping protect your system.

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

---

**Instatic enforces plugin permissions and network host allowlists through a two-layer security model that validates manifest declarations at install time and uses runtime gatekeeping in the QuickJS sandbox to block unauthorized operations.**

In the CoreBunch/Instatic repository, every plugin operates inside a sandboxed QuickJS environment where security is non-negotiable. The system implements a dual-phase enforcement strategy that first declares constraints in the plugin manifest and then actively gates every privileged operation against those constraints. This approach ensures that even if a plugin is compromised, it cannot exceed its explicitly granted capabilities or communicate with unauthorized external hosts.

## Declaring Permissions and Network Hosts in the Manifest

Every Instatic plugin must declare its required capabilities upfront in [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json). This static declaration serves as the foundation for the runtime enforcement system.

### The plugin.json Schema

The manifest schema, defined in [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts), requires two critical security fields:

- **`permissions`**: An array of capability strings (e.g., `network.outbound`, `cms.routes`) that the plugin requests.
- **`networkAllowedHosts`**: An array of host patterns (supporting `*.<domain>` wildcards) that the plugin is permitted to contact via HTTP if granted the `network.outbound` permission.

When an administrator installs a plugin, the system validates these declarations and stores the **granted permissions** in the plugin’s runtime context. Only permissions explicitly approved by the host admin become available to the plugin at runtime.

```json
{
  "id": "example.plugin",
  "version": "1.0.0",
  "entrypoints": { "editor": "src/index.ts" },
  "permissions": ["network.outbound", "cms.routes"],
  "networkAllowedHosts": ["api.example.com", "*.cdn.example.com"]
}

```

### The definePlugin Builder

The [`src/core/plugin-sdk/builders/definePlugin.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/builders/definePlugin.ts) helper merges these declarations into the final manifest object that the core runtime consumes. This builder ensures that both `permissions` and `networkAllowedHosts` are properly typed and included in the plugin package sent to the server.

## Runtime Permission Enforcement via PluginContext

Once a plugin is activated, the core runtime creates a `PluginContext` that contains only the **granted permissions** subset. This context is injected into every lifecycle hook and API call as `api.plugin`, making `api.plugin.permissions` the authoritative source of truth for capability checks.

The enforcement logic lives in [`src/core/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts), which constructs the context by reading the manifest’s `grantedPermissions` field and filtering out any permissions not explicitly approved by the admin. All privileged operations inside the sandbox must verify their presence in this array before proceeding.

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

export default definePlugin({
  manifest: { /* ... */ },
  async setup(api) {
    // `api.plugin.permissions` contains only the granted set
    if (api.plugin.permissions.includes('network.outbound')) {
      // safe to perform outbound fetch – host will be validated next
      const data = await api.fetch('https://api.example.com/data');
      console.log(data);
    } else {
      console.warn('Missing network.outbound permission');
    }
  },
});

```

The integrity of this system is verified by the test suite in [`src/__tests__/server/pluginVmPermissions.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/pluginVmPermissions.test.ts), which asserts that only granted permissions are exposed via `api.plugin.permissions` and that unlisted permissions remain inaccessible.

## Network Host Allowlist Enforcement in the Sandbox

Outbound HTTP requests face a second layer of scrutiny. Even if a plugin possesses the `network.outbound` permission, it cannot contact arbitrary hosts. The QuickJS sandbox’s fetch polyfill forwards all requests to a host bridge implemented in [`server/plugins/host/network.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/host/network.ts), which enforces the `networkAllowedHosts` allowlist.

### The guardedFetch Gatekeeper

The `guardedFetch` function in [`server/plugins/host/network.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/host/network.ts) parses the request URL and validates the host against the manifest’s allowlist using `hostMatches`, which supports `*.<domain>` wildcard patterns. If the host is not present in the list, the function throws a descriptive error and aborts the request before any network traffic occurs.

```typescript
// Called by the sandboxed fetch polyfill
export async function guardedFetch(url: string, manifest: PluginManifest) {
  const parsed = new URL(url);
  const allowlist = manifest.networkAllowedHosts ?? [];

  // Verify host against allowlist (supports "*.<domain>" wildcards)
  if (!allowlist.some(host => hostMatches(parsed.host, host))) {
    throw new Error(
      `Plugin "${manifest.id}" requested fetch to "${parsed.host}", ` +
      `which is not in the manifest's networkAllowedHosts allowlist.`
    );
  }

  // Host is allowed – perform the real fetch
  return fetch(url);
}

```

This two-step validation—**permission check** followed by **host validation**—ensures that compromised or malicious plugins cannot exfiltrate data to attacker-controlled domains even if they escape other sandbox restrictions.

## Content Security Policy Integration for Published Pages

Instatic extends security enforcement to the browser by updating the Content Security Policy (CSP) for published pages. The system aggregates all `networkAllowedHosts` entries from enabled plugins and injects them into the `connect-src` directive via [`server/publish/frontendInjections.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/frontendInjections.ts).

This union approach ensures that client-side code running in published pages can only connect to hosts explicitly approved by the collective plugin manifests, preventing cross-origin data leakage.

```typescript
const networkAllowedHostsSet = new Set<string>();
for (const plugin of enabledPlugins) {
  for (const host of plugin.manifest.networkAllowedHosts ?? []) {
    networkAllowedHostsSet.add(host);
  }
}
const plan = {
  networkAllowedHosts: [...networkAllowedHostsSet].sort(),
  // …
};

addCspSources(csp, 'connect-src', ["'self'", ...toCspHostSources(plan.networkAllowedHosts)]);

```

## Summary

Instatic implements a defense-in-depth strategy for plugin security through these key mechanisms:

- **Static Manifest Validation**: Plugins must declare `permissions` and `networkAllowedHosts` in [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) according to the schema in [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts).
- **Runtime Permission Context**: The `PluginContext` created in [`src/core/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts) exposes only granted permissions via `api.plugin.permissions`, which all privileged code must check.
- **Network Gatekeeping**: The `guardedFetch` function in [`server/plugins/host/network.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/host/network.ts) enforces host allowlists with wildcard support, rejecting unauthorized outbound requests with clear error messages.
- **CSP Union Injection**: Published pages receive a hardened CSP via [`server/publish/frontendInjections.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/frontendInjections.ts) that restricts `connect-src` to the union of all plugin-declared hosts.

## Frequently Asked Questions

### What happens if a plugin requests a host not in the allowlist?

The `guardedFetch` function in [`server/plugins/host/network.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/host/network.ts) intercepts the request, compares the target host against the manifest’s `networkAllowedHosts` array, and throws a descriptive error if no match is found. The error propagates to the plugin’s error-handling flow, and no network packet leaves the sandbox.

### How does Instatic handle wildcard subdomains in networkAllowedHosts?

The host validation logic supports `*.<domain>` patterns via the `hostMatches` utility function. When validating a fetch request, `guardedFetch` iterates through the allowlist and permits the connection if the target host matches either an exact entry or a wildcard pattern (e.g., `*.cdn.example.com` matches `assets.cdn.example.com`).

### Can a plugin gain additional permissions after installation?

No. The `grantedPermissions` array is fixed at install time and baked into the `PluginContext` by [`src/core/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/runtime.ts). Any attempt to access capabilities not in the original grant results in a missing permission check failure. Administrators must reinstall or update the plugin with a new manifest to change permission grants.

### Where is the permission enforcement tested?

The core runtime permission injection is verified in [`src/__tests__/server/pluginVmPermissions.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/pluginVmPermissions.test.ts), which ensures that `api.plugin.permissions` contains exactly the subset of manifest permissions that were granted by the administrator, and that unlisted permissions remain inaccessible to plugin code.