How Instatic Enforces Plugin Permissions and Network Host Allowlists: A Two-Layer Security Model
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. 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, 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 thenetwork.outboundpermission.
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.
{
"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 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, 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.
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, 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, which enforces the networkAllowedHosts allowlist.
The guardedFetch Gatekeeper
The guardedFetch function in 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.
// 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.
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.
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
permissionsandnetworkAllowedHostsinplugin.jsonaccording to the schema insrc/core/plugins/manifest.ts. - Runtime Permission Context: The
PluginContextcreated insrc/core/plugins/runtime.tsexposes only granted permissions viaapi.plugin.permissions, which all privileged code must check. - Network Gatekeeping: The
guardedFetchfunction inserver/plugins/host/network.tsenforces 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.tsthat restrictsconnect-srcto 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 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. 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, 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →