Security & Isolation
OpenGPEX enforces a layered security model to protect users from malicious or buggy plugins while preserving the flexibility of the plugin system.
Three-Tier Safety Net
Tier 1: Safe Mode (URL Escape Hatch)
If a plugin causes the editor to crash on startup, users can bypass it:
http://localhost:3000/?safe-mode=true
In Safe Mode:
- All official core plugins (
base+community) still load — Safe Mode excludes every user plugin - Both static
userplugins and dynamically uploaded ZIP plugins are skipped entirely (usePluginInitnever registers them), so their commands and shortcuts are absent - Console outputs
⚠️ [OpenGPEX Safe Mode]warning - User can then disable/remove the problematic plugin
Tier 2: UI Error Boundary
Every plugin component is wrapped in <PluginErrorBoundary>:
- If a plugin throws during React render, only that plugin crashes
- The rest of the editor (canvas, toolbars, other plugins) continues normally
- The crashed area shows a warning icon with the plugin ID
- User can save work and disable the plugin from GPEX Hub
Tier 3: Command Execution Isolation
All command execution is wrapped in try/catch within the core dispatcher:
// In useEditorStore.ts — executeCommand
try {
return command.execute(scopedContext, payload);
} catch (error) {
console.error(`[OpenGPEX Error] Command Execution Failed (${id}):`, error);
return undefined; // Prevents crash propagation
}
Interceptors (beforeExecute) are also independently try/caught.
Trust Levels (sourceType)
| Level | Meaning | Capabilities |
|---|---|---|
base |
Official core plugins | Full access, cannot be disabled |
community |
Verified third-party | Full access, user can disable |
user |
User-installed | Sandboxed metadata, user can remove |
Cloud Mode Restrictions
When running in GPEX-Cloud SaaS mode (NEXT_PUBLIC_GPEX_CLOUD_MODE=true, or server-side GPEX_CLOUD_MODE=true — see core/helpers/config.ts):
- ❌ Plugin upload API (
/api/plugins/upload) returns 403 Forbidden - ❌ Dynamic plugin loading is skipped entirely (
usePluginInitnever callsloadUserPlugins()in cloud mode) - ❌ Upload UI button is physically hidden (
IS_CLOUD_MODEgate in GPEX Hub panel) - ✅ Pre-installed
baseandcommunityplugins function normally - ✅ This prevents untrusted code from executing in the shared cloud environment
Upload Defenses (Server Side)
The /api/plugins/upload route applies layered validation before anything touches disk:
| Defense | Behavior |
|---|---|
| Manifest validation | ZIP must contain plugin.manifest (or plugin.json) parseable as JSON, with both id and author — otherwise 400 |
| Namespace blocklist | id starting with base. or com.gpex.plugins.base. is rejected (400) |
| Zip Slip check | Every entry is normalized; paths containing .. or absolute paths are rejected, and the final write path is re-validated to stay inside the plugins directory |
| Flat/nested extraction | ZIPs without a shared root folder are extracted into a sandbox folder derived from the plugin's local ID |
| Static conflict detection | If an uploaded plugin collides with a statically registered user plugin UID, the upload succeeds and the conflicting static version is auto-disabled |
Uploaded plugins are persisted under PERSISTENT_PLUGINS_DIR (a compile-time constant, data/plugins/user, in core/helpers/config.ts).
Namespace Protection
| Rule | Enforcement |
|---|---|
base.* namespace reserved |
Packager refuses to build; upload API rejects |
Dynamic plugins get forced sourceType: 'user' |
Loader overwrites any attempts to impersonate base |
UID computed from author + id |
Prevents two plugins from claiming the same namespace |
Metadata Sandboxing (Dynamic Plugins)
When a ZIP plugin is dynamically loaded, the system forces:
- UID override: Computed fresh from manifest (prevents hijacking existing plugins)
- sourceType = 'user': Cannot masquerade as
baseorcommunity - group = 'gpex.plugins.user.': Forced into user namespace group
Plugin Configuration Access
- Plugins access their own config via
usePluginSelfConfig()(zero-argument) - Reading another plugin's config is possible via
usePluginConfig(uid)but modifying it directly is discouraged - Cross-plugin interaction should flow through:
- Command Bus — Explicit, auditable actions
- Public Signals — Read-only state indicators for consumers
Runtime Environment
Plugins run in the same JavaScript context as the core engine (no iframe or Worker sandboxing). Isolation is enforced by convention:
- Accessing DOM outside the plugin's slot → discouraged
- Importing store internals directly → discouraged
- Network access (fetch/XHR) → allowed (standard browser sandbox applies)
- Local file system access → browser-restricted (requires user prompt)
Best Practices for Plugin Developers
- Don't abuse Error Boundaries — Handle known failure cases (network, JSON parse) with your own try/catch and show toast notifications
- Clean up event listeners — Always remove
window.addEventListenerin useEffect cleanup functions - Isolate side effects — Don't modify state outside your plugin's scope (
state.frames, other plugins' config) - Use
try...finallyfor async commands — Always callctx.scoped.setBusy(false)in the finally block
Next Steps
- Plugin System Overview — Trust model architecture
- Plugin Packaging — Distribution requirements
- Plugin API Reference — Complete hook documentation
Last updated: 2026-07-30