# How Instatic Handles Plugin Permissions and Network Access

> Instatic secures plugins with QuickJS-WASM sandboxing and a fail-closed permission model. Learn how explicit grants prevent network access and SSRF attacks.

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

---

**TLDR:** Instatic isolates each plugin in a QuickJS-WASM sandbox and enforces a fail-closed permission model defined in the plugin's manifest, requiring explicit grants for capabilities like `network.outbound` and validating every HTTP request against host allow-lists and blocked IP ranges to prevent SSRF attacks.

Instatic is a CMS that extends functionality through isolated plugins. According to the CoreBunch/Instatic source code, the platform implements a defense-in-depth strategy for plugin permissions and network access, combining manifest declarations with runtime validation and Content Security Policy enforcement.

## Manifest-Driven Permission Grants

Instatic's permission system begins with the **PluginManifest** schema defined in [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts). Each plugin declares its required capabilities in an optional `grantedPermissions` array within the manifest.

During installation, the UI collects the permissions a plugin requests and stores the granted subset in the database alongside the manifest. This persistence layer is implemented in [`src/core/persistence/cmsPlugins.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/persistence/cmsPlugins.ts):

```typescript
// src/core/persistence/cmsPlugins.ts (excerpt)
if (grantedPermissions.length > 0) {
  // Store the granted list alongside the manifest
  formData.set('grantedPermissions', JSON.stringify(grantedPermissions));
}
await apiRequest('/api/cms/plugins', { method: 'POST', body: formData });

```

The granted permissions are stored with the plugin record, creating an audit trail of what the administrator explicitly approved.

## Runtime Permission Enforcement

When core code performs actions on behalf of a plugin, it validates the granted permission set before execution. This check occurs in multiple subsystems.

### Admin-Side Hook Validation

The admin interface guards plugin hook handlers in [`src/admin/plugin-host-hooks/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/plugin-host-hooks/index.ts). Before invoking a handler, the dispatcher verifies the specific permission exists in the context:

```typescript
if (!ctx.grantedPermissions.includes(permission)) {
  // Permission denied
}

```

### Editor-Side Module Guards

Similarly, the editor loader checks permissions before allowing module registration or code editing capabilities. The [`src/core/plugins/editorPluginLoader.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/editorPluginLoader.ts) and [`src/core/plugins/moduleAdapter.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/moduleAdapter.ts) files enforce these restrictions during plugin initialization:

```typescript
// src/core/plugins/moduleAdapter.ts (excerpt)
if (!grantedPermissions.includes('frontend.assets')) {
  throw new Error('Plugin lacks permission to load frontend assets');
}

```

## Network Outbound Access Control

A plugin may request the `network.outbound` permission, but actual HTTP requests undergo two additional validation layers.

### Host Allow-List Validation

The manifest must contain a `networkAllowedHosts` array specifying approved destinations. The host bridge in [`src/server/plugins/host/network.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/server/plugins/host/network.ts) validates every request URL against this list using the `hostMatchesAllowlist` function, which supports wildcards like `*.example.com` but restricts matching to hostnames only (no ports, paths, or query strings).

The core validation routine `assertOutboundAllowed` (lines 26-58 of [`network.ts`](https://github.com/CoreBunch/Instatic/blob/main/network.ts)) parses the URL and enforces these constraints.

### IP Address Blocking

To prevent Server-Side Request Forgery (SSRF) attacks, the system resolves the target host to IP addresses and blocks any address falling into private, loopback, link-local, or CGNAT ranges using the `isBlockedAddress` function. If the host resolves to a blocked range, the plugin receives an error before any network connection attempts.

The gated fetch implementation demonstrates this flow:

```typescript
// server/plugins/host/network.ts (excerpt)
export async function performGatedFetch(
  entry: HostPluginRecord,
  urlString: string,
  init: { method?: string; headers?: Record<string, string>; body?: string },
) {
  // 1️⃣ Ensure the plugin has the network permission (asserted by caller)
  // 2️⃣ Validate target against allow-list & blocked IP ranges
  const parsed = await assertOutboundAllowed(entry.manifest, urlString, defaultResolveHost);
  // 3️⃣ Execute the real fetch and serialize the response for the VM
  const resp = await fetchImpl(parsed.toString(), { method: init.method, headers: init.headers });
  return { status: resp.status, ok: resp.ok, headers: Object.fromEntries(resp.headers), body: await resp.text(), bodyEncoding: 'utf8' };
}

```

## Content Security Policy Integration

Instatic extends network controls to the client side during publication. The system aggregates all `networkAllowedHosts` from enabled plugins and injects them into the Content Security Policy (CSP) `connect-src` directive.

This enforcement occurs in [`src/server/publish/frontendInjections.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/server/publish/frontendInjections.ts):

```typescript
// src/server/publish/frontendInjections.ts (excerpt)
const networkAllowedHostsSet = new Set<string>();
for (const plugin of enabledPlugins) {
  for (const host of plugin.manifest.networkAllowedHosts ?? []) {
    if (host) networkAllowedHostsSet.add(host);
  }
}
plan.networkAllowedHosts = [...networkAllowedHostsSet].sort();
addCspSources(csp, 'connect-src', ["'self'", ...toCspHostSources(plan.networkAllowedHosts)]);

```

This ensures that client-side code generated by plugins cannot bypass the same host restrictions enforced server-side.

## Security-By-Design Principles

The permission model is deliberately **fail-closed**:

- If `networkAllowedHosts` is empty or missing, outbound fetches are denied entirely.
- The host-match logic rejects localhost and IP literals at the manifest validation stage.
- The address-blocking logic prevents SSRF attacks by refusing connections to internal networks.

Together, these layers guarantee that a plugin can only perform actions explicitly approved by both the manifest author and the site administrator.

## Summary

- **Manifest-based permissions**: Instatic uses the `grantedPermissions` array in `PluginManifest` to track approved capabilities.
- **Runtime validation**: Both admin hooks ([`src/admin/plugin-host-hooks/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/plugin-host-hooks/index.ts)) and editor modules ([`src/core/plugins/moduleAdapter.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/moduleAdapter.ts)) verify permissions before execution.
- **Network gates**: The `network.outbound` permission requires accompanying `networkAllowedHosts` entries, validated by `assertOutboundAllowed` in [`src/server/plugins/host/network.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/server/plugins/host/network.ts).
- **SSRF protection**: The `isBlockedAddress` function blocks private and loopback IP ranges after DNS resolution.
- **CSP enforcement**: Allowed hosts are aggregated at publish time and injected into the `connect-src` directive via [`src/server/publish/frontendInjections.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/server/publish/frontendInjections.ts).

## Frequently Asked Questions

### What happens if a plugin requests network access but has no allowed hosts defined?

If the manifest's `networkAllowedHosts` array is empty or missing, the `assertOutboundAllowed` function denies all outbound requests, even if the `network.outbound` permission is granted. This fail-closed design prevents unrestricted internet access.

### How does Instatic prevent plugins from accessing internal network resources?

The system resolves target hostnames to IP addresses and validates them against `isBlockedAddress`, which rejects private ranges (RFC 1918), loopback addresses (127.0.0.0/8), link-local addresses (169.254.0.0/16), and CGNAT ranges (100.64.0.0/10). This blocks SSRF attacks attempting to reach internal services.

### Can plugins use wildcards in the network allow-list?

Yes, the `hostMatchesAllowlist` function supports wildcard patterns like `*.example.com`. However, matching applies only to the hostname component; ports, paths, and query parameters are not considered, and IP literals are rejected during manifest validation.

### Where are granted permissions stored persistently?

The granted permission subset is stored alongside the plugin manifest in the database via [`src/core/persistence/cmsPlugins.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/persistence/cmsPlugins.ts). This storage captures the explicit approvals collected during the installation UI flow, ensuring permissions persist across server restarts and are available for runtime checks.