Security Considerations for Instatic Plugins: Architecture and Implementation
Instatic isolates third-party plugins inside QuickJS-WASM sandboxes with strict permission manifests, capability gating, and static analysis to prevent arbitrary code execution.
The CoreBunch/Instatic repository implements a defense-in-depth security model that balances extensibility with host protection. This article examines the technical mechanisms—from VM isolation to manifest validation—that prevent malicious or buggy plugins from compromising the core system.
Sandbox Isolation with QuickJS-WASM
Instatic loads each plugin into a QuickJS-WASM VM that has zero access to Node.js or Bun native APIs, the filesystem, or process bindings. According to server/plugins/runtime.ts, the activation process boots plugins inside this VM context, exposing only a narrow SDK surface while preventing escape to the underlying runtime environment.
The sandbox explicitly lacks bindings to process, fs, or any native modules, ensuring that even if plugin code attempts to require system resources, the execution context prevents arbitrary code execution.
Permission Manifest and Capability Gating
Every plugin must declare its required capabilities in a plugin.json manifest file. The host validates these permissions at install time and enforces them at runtime through capability gating defined in src/core/capabilities.ts.
Declaring Permissions
As documented in examples/plugins/template/README.md, the manifest uses an explicit permission list pattern. Valid capabilities include editor.commands, cms.routes.public, and site.structure.edit:
{
"id": "acme.my-plugin",
"name": "My Awesome Plugin",
"version": "1.0.0",
"permissions": [
"editor.commands",
"cms.routes.public",
"plugins.configure"
],
"entrypoints": {
"server": "dist/server.js",
"editor": "dist/editor.js"
}
}
Runtime Capability Checks
Core capabilities are verified via requireCapability or requireAnyCapability before any privileged operation. The docs/reference/capabilities.md file documents the full permission matrix available to plugins.
For example, deleting CMS content requires the site.structure.edit capability:
import { requireCapability } from '@core/capabilities';
export async function deletePage(pageId: string) {
requireCapability('site.structure.edit');
// ...perform deletion via core API...
}
SDK calls are mapped to capabilities in apiDispatch.ts, ensuring consistent enforcement across all plugin entry points.
Network Restrictions and Static Analysis
Network Whitelisting
By default, plugin VMs cannot make outbound HTTP requests. The server/plugins/quickjs/vm.ts file implements a networkAllowedHosts whitelist that restricts fetch operations to explicitly declared domains in the plugin manifest.
Static Bundle Vetting
Before installation, Instatic scans plugin bundles for forbidden patterns including node:, bun:, require(, and process.binding. The test suite src/__tests__/architecture/plugin-sandbox-invariants.test.ts guarantees that no forbidden imports slip through validation, effectively blocking attempts to import native modules.
Error Isolation and Content Access Control
Fault Isolation
Plugin failures are caught and logged with a [plugin:<id>] prefix without crashing the server. As implemented in server/plugins/runtime.ts, a misbehaving plugin is disabled while the host continues running other extensions.
Content Security Policies
CRUD operations on CMS data require explicit cms.content.* permissions. The system enforces these centrally in apiDispatch.ts and at the handler level via assertContentTableAccess, ensuring plugins only access content tables explicitly allowed by the administrator.
Manifest Validation and Upload Security
Schema Validation
The parsePluginManifest function in src/core/plugins/manifest.ts uses TypeBox schemas to validate that only well-formed JSON is accepted and that declared permissions match the allowed set defined by the core system.
Secure Plugin Distribution
Plugin bundles are served from /uploads/plugins/* with Access-Control-Allow-Origin: * restrictions because they execute within sandboxed iframes configured with sandbox="allow-scripts". According to server/plugins/package.ts, this prevents same-origin access and data leakage between plugins and the host application.
Summary
- QuickJS-WASM isolation in
server/plugins/runtime.tsprevents access to Node.js, Bun, filesystem, or native bindings - Explicit permission manifests in
plugin.jsonrequire administrators to consent to each capability before installation - Capability gating via
requireCapabilityenforces runtime permission checks for all privileged operations - Network whitelisting blocks outbound HTTP requests to undeclared hosts by default
- Static analysis in
plugin-sandbox-invariants.test.tsscans bundles for forbidden imports before activation - Error isolation ensures plugin crashes do not affect the host server or other plugins
- Schema validation rejects malformed manifests and invalid permission strings at install time
Frequently Asked Questions
How does Instatic prevent plugins from accessing the filesystem?
Instatic loads plugins into a QuickJS-WASM VM that lacks bindings to Node.js or Bun filesystem APIs. The runtime exposes only a narrow SDK surface, and static analysis explicitly scans for forbidden patterns like node: imports or process.binding calls that could provide filesystem access.
What happens if a plugin tries to make an unauthorized network request?
The VM configured in server/plugins/quickjs/vm.ts maintains a networkAllowedHosts whitelist parsed from the plugin manifest. If a plugin attempts to fetch from a host not declared in its permissions, the runtime blocks the request, preventing data exfiltration or unauthorized API calls.
Can a malicious plugin crash the entire Instatic server?
No. Error handling in server/plugins/runtime.ts catches all plugin exceptions with [plugin:<id>] prefixes. A malfunctioning plugin is isolated and disabled without terminating the host process or affecting other installed plugins.
How are plugin permissions validated during installation?
The parsePluginManifest function in src/core/plugins/manifest.ts validates the plugin.json against a TypeBox schema that enforces allowed permission strings. Unknown capabilities are rejected at install time, ensuring the permission system cannot be bypassed through malformed manifests.
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 →