Files
archy/docs/research/plugin-security.md
T
DorianandClaude Opus 4.6 4ac18cedb6 docs: add research docs for iOS app, Mac desktop, plugin security
- iOS: Capacitor vs WKWebView vs React Native WebView analysis
- Mac: Tauri v2 vs Electron comparison with menu bar app patterns
- Plugins: Signature validation, sandboxed iframes, permission system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:56:54 +00:00

7.3 KiB

Plugin System Hardening Research — AIUI

Current State

The plugin system has these existing components:

  • packages/core/src/types/plugin.tsAIUIPlugin interface, PluginContext, PluginType
  • packages/core/src/plugins/registry.ts — in-memory Vue ref-based registry
  • packages/app/src/stores/pluginMarketplace.tsInstalledPlugin, PluginPermission, installPlugin, hasPermission
  • packages/app/src/components/settings/PluginMarketplace.vue — permissions dialog
  • packages/app/src/components/renderers/CodeRunner.vue — existing <iframe sandbox="allow-scripts"> + postMessage pattern

Gaps: No cryptographic signature verification, no runtime permission enforcement in sandbox, no CSP on plugin iframes.

1. Signature Validation for Community Plugins

Problem

importFromUrl fetches arbitrary aiui-plugin.json from any URL with no integrity check. A compromised URL grants arbitrary code execution.

Aligns with existing crypto posture (tweetnacl.js for E2E, Web Crypto for storage).

Manifest format:

{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.2.0",
  "signature": {
    "algorithm": "ed25519",
    "publicKey": "base64-public-key",
    "value": "base64-signature-over-canonical-manifest"
  }
}

Verification: Canonical payload = manifest JSON minus signature field, sorted keys. Verify via crypto.subtle.verify('Ed25519', ...) (Chrome 113+, Firefox 130+, Safari 17+) with tweetnacl.js fallback.

Integration point: Gate installPlugin() on signature verification for Tier 2+ plugins.

Secondary: SRI Hash

bundleUrl + integrity field enables browser-native subresource integrity enforcement at load time.

Trust Model

Tier Who Signing Sandbox Verification
1 — Built-in AIUI maintainers Bundled in app None (trusted) None needed
2 — Verified Reviewed by AIUI team Ed25519 by author Full iframe sandbox Signature + hash
3 — Sideloaded User-imported URL Optional Full iframe sandbox, stricter CSP Warn prominently

2. Sandboxed iframe Execution

Architecture

Each community plugin runs in an isolated iframe. AIUI manages a PluginBridge service:

AIUI App (parent)
    │
    │  postMessage (structured protocol)
    │
    └── Plugin Sandbox iframe (origin: null, sandbox="allow-scripts")
            └── Plugin code (no DOM, no storage, no cross-origin network)

Sandbox Configuration

<!-- Tier 2 plugin (headless, no UI) -->
<iframe sandbox="allow-scripts" srcdoc="..." style="display: none" />

<!-- Tier 2 plugin with UI panel -->
<iframe sandbox="allow-scripts allow-popups-to-escape-sandbox" srcdoc="..." />

Never grant: allow-same-origin (breaks isolation), allow-forms, allow-top-navigation, allow-modals unless explicitly user-approved.

postMessage Protocol

Typed message envelopes following the archyBridge pattern:

// Plugin → Host
interface PluginRequest {
  type: 'plugin:request'
  id: string          // correlation ID
  pluginId: string
  capability: string  // e.g. 'storage:get', 'network:fetch'
  payload: unknown
}

// Host → Plugin
interface PluginResponse {
  type: 'plugin:response'
  id: string
  success: boolean
  data?: unknown
  error?: string
}

Host-side Validation

  1. event.origin must be null (sandboxed srcdoc iframes)
  2. pluginId must match the iframe→plugin mapping
  3. Requested capability must be in grantedPermissions
  4. Rate-limit: reject if > N requests/second (DoS prevention)

Network Restriction

With sandbox="allow-scripts" alone, iframes can still fetch(). Block direct network via CSP in srcdoc:

<meta http-equiv="Content-Security-Policy"
  content="default-src 'none'; script-src 'unsafe-inline'; connect-src 'none'">

Plugins requiring network use the network:fetch capability — host proxies the request after validating the URL.

Storage Isolation

Sandboxed iframes without allow-same-origin cannot access parent's localStorage/IndexedDB. Plugin storage goes through the storage capability, namespaced under plugin::{id}::.

3. Permission System Per Plugin

Expanded Permissions

export type PluginPermission =
  | 'chat-read'        // read chat history
  | 'chat-inject'      // inject messages (high risk)
  | 'chat-messages'    // read + inject (legacy, maps to both)
  | 'network'          // outbound HTTPS via host proxy
  | 'favorites'        // read/write favorites
  | 'storage'          // namespaced plugin storage
  | 'nostr'            // access Nostr identity (high risk)
  | 'wallet'           // deep-link to wallet (high risk)
  | 'clipboard'        // read/write clipboard
  | 'notifications'    // send notifications
  | 'media-playback'   // control media player
  | 'renderer'         // register content renderer
  | 'settings-read'    // read AIUI settings

Risk Classification

const permissionRisk: Record<PluginPermission, 'low' | 'medium' | 'high'> = {
  'network': 'low',
  'storage': 'low',
  'chat-read': 'low',
  'favorites': 'low',
  'notifications': 'medium',
  'clipboard': 'medium',
  'media-playback': 'medium',
  'renderer': 'medium',
  'settings-read': 'medium',
  'chat-messages': 'high',
  'chat-inject': 'high',
  'nostr': 'high',
  'wallet': 'high',
}
  1. High-risk permissions default to unchecked in consent dialog
  2. Show risk badges (low/medium/high) next to each permission
  3. Unverified plugins (Tier 3) show prominent warning before permissions dialog
  4. Progressive disclosure: summary before detailed checkboxes

Permission Revocation

  1. Terminate plugin iframe immediately (iframe.remove())
  2. Host handler rechecks hasPermission() on every capability call
  3. Emit plugin:permission-revoked event so plugin can react gracefully

Least Privilege

  • Plugins declare minimum permissions in manifest
  • All capabilities gated on hasPermission() — no admin override
  • Storage namespaced under plugin::{id}::
  • Optional allowedOrigins[] in manifest restricts network targets
  • Audit log: capability invocations logged with plugin ID

Integration Points

Files to modify:

packages/core/src/types/plugin.ts — Add tier, signature, sandbox fields to manifest type.

packages/app/src/stores/pluginMarketplace.ts — Add verifyManifestSignature() in installPlugin(). Add revokePermission(). Move grantedPermissions to encrypted storage.

packages/core/src/plugins/registry.ts — Evolve into PluginHost service that manages sandbox iframe lifecycle and routes postMessage capability requests.

packages/app/src/plugins/index.ts — Tier 1 plugins use registerPlugin() directly. Tier 2+ load through PluginHost.loadSandboxed(manifest).

Concrete Next Steps

  1. Define PluginSignature type and verifyManifestSignature() using tweetnacl.js
  2. Create PluginSandbox service to manage iframe lifecycle + postMessage routing
  3. Add CSP meta tag to plugin srcdoc template
  4. Implement capability handlers (storage, network, chat) with permission checks
  5. Update consent dialog with risk classification badges
  6. Add revokePermission() with live iframe termination
  7. Create CLI tool for plugin authors to sign manifests