|
| 1 | +# Security Model & Threat Analysis |
| 2 | + |
| 3 | +## Threat Model |
| 4 | + |
| 5 | +### Assets |
| 6 | +- Host filesystem (read/write/execute) |
| 7 | +- Host network (internal/external) |
| 8 | +- Host process execution |
| 9 | +- Plugin integrity (supply chain) |
| 10 | +- User data in plugin tabs |
| 11 | + |
| 12 | +### Actors |
| 13 | +- **Malicious plugin author**: Publishes plugin to registry |
| 14 | +- **Compromised registry**: GitHub repo / CDN hijacked |
| 15 | +- **Local attacker**: Code execution on host machine |
| 16 | +- **Network attacker**: MITM on plugin download |
| 17 | + |
| 18 | +### Trust Boundaries |
| 19 | +``` |
| 20 | +┌─────────────────────────────────────────────────────┐ |
| 21 | +│ HOST OS │ |
| 22 | +│ ┌─────────────────────────────────────────────┐ │ |
| 23 | +│ │ PLUG RUNTIME (Rust) │ │ |
| 24 | +│ │ ┌─────────────────────────────────────┐ │ │ |
| 25 | +│ │ │ │ │ WASM SANDBOX (Wasmer) │ │ │ |
| 26 | +│ │ │ - Linear memory (isolated) │ │ │ |
| 27 | +│ │ │ - No direct syscalls │ │ │ |
| 28 | +│ │ │ - Host imports ONLY via FFI gate │ │ │ |
| 29 | +│ └─────────────────────────────────────┘ │ |
| 30 | +│ │ ┌─────────────────────────────────────┐ │ │ |
| 31 | +│ │ │ PERMISSION GATE │ │ │ |
| 32 | +│ │ │ - Load-time import validation │ │ │ |
| 33 | +│ │ │ - Call-time runtime checks │ │ │ |
| 34 | +│ │ │ - WASI allowlist enforcement │ │ │ |
| 35 | +│ │ └─────────────────────────────────────┘ │ |
| 36 | +│ └─────────────────────────────────────────────┘ |
| 37 | +└─────────────────────────────────────────────────────┘ |
| 38 | +``` |
| 39 | + |
| 40 | +## Security Controls |
| 41 | + |
| 42 | +### 1. WASM Sandbox (Wasmer 4.3 Cranelift) |
| 43 | +- Linear memory isolation (no host pointer access) |
| 44 | +- No direct syscall instruction execution |
| 45 | +- All host interaction via explicit FFI imports |
| 46 | + |
| 47 | +### 2. Import Validation (Load Time) |
| 48 | +**Env namespace** (`env.*`): |
| 49 | +- Every import checked against manifest `permissions[]` |
| 50 | +- Missing permission → load failure |
| 51 | +- Imports: `host_exec`, `host_add_tab`, `host_set_tab_owner`, `host_get_tab_label`, `host_get_platform`, `get_env`, `net_post`, `print_info`, `print_error`, `get_args` |
| 52 | + |
| 53 | +**WASI namespace** (`wasi_snapshot_preview1.*`): |
| 54 | +- **Explicit allowlist only** (see `plugin_mgr.rs:ALLOWED_WASI`) |
| 55 | +- Blocked: `path_open`, `path_readlink`, `path_rename`, `path_unlink_file`, `path_create_directory`, `path_remove_directory`, `path_symlink`, `path_link`, `sock_connect`, `sock_bind`, `sock_listen`, `sock_accept`, `proc_raise`, `random_get` (stubbed), etc. |
| 56 | +- Allowed: `fd_write`/`fd_read` (stdout/stderr only), `proc_exit`, `clock_time_get`, `args_*`, `environ_*` (stubs), `poll_oneoff`, `sched_yield`, `sock_*` (stubs returning ENOSYS) |
| 57 | + |
| 58 | +### 3. Runtime Gates (Call Time) |
| 59 | +Each sensitive import re-checks permission before executing: |
| 60 | +```rust |
| 61 | +if !env_data.permissions.iter().any(|p| p == "host_exec") { |
| 62 | + print_error("[SECURITY] Plugin attempted to call host_exec without permission"); |
| 63 | + return; |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +### 4. Command Execution Hardening (`host_exec`) |
| 68 | +- **No shell**: Direct `Command::new(exe).args(args)` — no `cmd /c`, `sh -c` |
| 69 | +- **Allowlist-only**: Manifest `allowed_commands` with canonical path + args regex |
| 70 | +- **Path canonicalization**: `resolve_binary_path()` → `fs::canonicalize()` |
| 71 | +- **No blacklist**: Blacklists are bypassable; removed entirely |
| 72 | + |
| 73 | +### 5. Network Hardening (`net_post`) |
| 74 | +- HTTPS only (scheme validation via `url::Url`) |
| 75 | +- Private IP blocking (RFC1918, RFC3927, RFC6598, loopback, multicast, reserved) |
| 76 | +- Hostname blocking: `localhost`, `localhost.localdomain` |
| 77 | +- Response size limit: 1 MiB |
| 78 | +- Timeout: 30s (configurable via `DEFAULT_TIMEOUT`) |
| 79 | + |
| 80 | +### 6. Filesystem Containment |
| 81 | +- `cd` command: `canonicalize()` + prefix check against process CWD |
| 82 | +- No WASI `path_*` functions exposed |
| 83 | +- Plugin working directory tracked per-tab (`TAB_CWDS`) |
| 84 | + |
| 85 | +### 7. Supply Chain Integrity |
| 86 | +- Registry (`pluglists.json`) signed with minisign/Ed25519 |
| 87 | +- Public key baked into binary (`REGISTRY_PUBKEY`) |
| 88 | +- Signature verified before parsing any registry content |
| 89 | +- Plugin WASM verified against registry-pinned SHA256 |
| 90 | +- Atomic write with same-FS verification (`write_atomic`) |
| 91 | + |
| 92 | +### 8. Input Validation |
| 93 | +- All FFI string reads bounded by constants: |
| 94 | + - `MAX_FFI_STRING_LEN = 64 KiB` |
| 95 | + - `MAX_URL_LEN = 2 KiB` |
| 96 | + - `MAX_JSON_PAYLOAD_LEN = 16 KiB` |
| 97 | + - `MAX_RESPONSE_BUF_LEN = 1 MiB` |
| 98 | + - `MAX_TAB_LABEL_LEN = 256 B` |
| 99 | +- Prevents OOB reads and allocation DoS |
| 100 | + |
| 101 | +## Known Limitations / Residual Risk |
| 102 | + |
| 103 | +| Risk | Mitigation | Residual | |
| 104 | +|------|-----------|----------| |
| 105 | +| WASI stubs return ENOSYS | Plugins expecting real syscalls fail gracefully | Low (breaks compat, not security) | |
| 106 | +| Allowlist regex ReDoS | `regex` crate is linear-time (no backtracking) | Low | |
| 107 | +| Registry key rotation | Not implemented; requires binary rebuild | Medium | |
| 108 | +| Side-channel via `host_get_platform` | Now permission-gated | Low | |
| 109 | +| TOCTOU in `write_atomic` cross-FS | Same-FS check + randomized temp name | Low | |
| 110 | +| Malicious plugin DoS (infinite loop) | No fuel metering / epoch interruption | Medium | |
| 111 | +| Memory exhaustion via large allocations | `MAX_FFI_STRING_LEN` bounds; Wasmer memory limit not set | Medium | |
| 112 | + |
| 113 | +## Security Checklist for Plugin Review |
| 114 | + |
| 115 | +- [ ] Manifest declares minimal permissions |
| 116 | +- [ ] `allowed_commands` uses canonical paths, restrictive regex |
| 117 | +- [ ] No WASI imports beyond allowlist (verify with `wasm-objdump -x plugin.wasm | grep wasi_snapshot_preview1`) |
| 118 | +- [ ] `net_post` URLs are HTTPS, external domains only |
| 119 | +- [ ] Plugin does not attempt `cd` traversal |
| 120 | +- [ ] SHA256 in registry matches published WASM |
| 121 | + |
| 122 | +## Incident Response |
| 123 | + |
| 124 | +1. **Malicious plugin detected**: Revoke registry entry, rotate minisign key, rebuild host |
| 125 | +2. **Registry compromise**: Rotate minisign key immediately, audit all plugins |
| 126 | +3. **Sandbox escape**: Isolate host, analyze WASM module, patch Wasmer/import gate |
0 commit comments