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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
290aa047d7
commit
4ac18cedb6
@@ -0,0 +1,209 @@
|
||||
# Plugin System Hardening Research — AIUI
|
||||
|
||||
## Current State
|
||||
|
||||
The plugin system has these existing components:
|
||||
- `packages/core/src/types/plugin.ts` — `AIUIPlugin` interface, `PluginContext`, `PluginType`
|
||||
- `packages/core/src/plugins/registry.ts` — in-memory Vue ref-based registry
|
||||
- `packages/app/src/stores/pluginMarketplace.ts` — `InstalledPlugin`, `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.
|
||||
|
||||
### Recommended: Ed25519 Detached Signatures
|
||||
|
||||
Aligns with existing crypto posture (tweetnacl.js for E2E, Web Crypto for storage).
|
||||
|
||||
**Manifest format:**
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```html
|
||||
<!-- 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:
|
||||
|
||||
```typescript
|
||||
// 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:
|
||||
|
||||
```html
|
||||
<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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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',
|
||||
}
|
||||
```
|
||||
|
||||
### User Consent Flow
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user