|
| 1 | +# DeepCode Security Model |
| 2 | + |
| 3 | +> Last updated: 2026-05-28 (M3.5 hardening + attack-vector test suite landed) |
| 4 | +
|
| 5 | +This document is the **single source of truth** for what DeepCode protects |
| 6 | +against, what it doesn't, and how each layer composes. If you're reviewing a |
| 7 | +PR that touches credentials, sandbox, plugin runtime, or hooks — verify it |
| 8 | +against the threat model here. |
| 9 | + |
| 10 | +## Threat model |
| 11 | + |
| 12 | +DeepCode is an LLM-driven coding assistant. The threats we care about, in |
| 13 | +decreasing order of operator severity: |
| 14 | + |
| 15 | +| # | Threat | Severity | Where mitigated | |
| 16 | +| - | ------------------------------------------------------------------------ | -------- | ----------------------- | |
| 17 | +| 1 | Model exfiltrates DeepSeek API key (or other env secrets) via tool call | High | M3.5 sandbox + M5.1 env strip | |
| 18 | +| 2 | Model writes arbitrary files outside the project (`/usr/bin`, `/etc`) | High | M3.5 sandbox + permissions | |
| 19 | +| 3 | Plugin (third-party code) does either #1 or #2 | High | M5.1 subprocess + (M5.1-ext) OS sandbox | |
| 20 | +| 4 | Hook script (third-party shell snippet) does either #1 or #2 | Medium | M3.5 sandbox wraps Bash; hooks bypass when invoked via /bin/sh directly | |
| 21 | +| 5 | Hostile `settings.json` field (e.g. allowRead path) injects sandbox rule | Medium | escapeSbpl() | |
| 22 | +| 6 | Untrusted project's AGENTS.md drives the agent into harmful action | Low | Trust store (`/trust`) | |
| 23 | +| 7 | DNS exfiltration of secrets from sandboxed Bash | Acknowledged limitation | M3.5-ext userspace proxy | |
| 24 | + |
| 25 | +## Defence layers |
| 26 | + |
| 27 | +### Layer 0 — Trust store |
| 28 | + |
| 29 | +First time DeepCode opens a folder, you're asked **"Do you trust this |
| 30 | +directory?"**. If you say no, the agent runs in a heavily restricted mode: |
| 31 | +no exec, no writes outside the project, no `bypassPermissions` mode allowed. |
| 32 | + |
| 33 | +Decisions persist in `~/.deepcode/trust.json`. |
| 34 | + |
| 35 | +### Layer 1 — Mode + Permissions |
| 36 | + |
| 37 | +Every tool call goes through: |
| 38 | + |
| 39 | +``` |
| 40 | + Mode policy → Permission rules → Sandbox wrap → Exec |
| 41 | +``` |
| 42 | + |
| 43 | +Modes (`default` | `acceptEdits` | `plan` | `auto` | `dontAsk` | `bypassPermissions`): |
| 44 | +- `plan` blocks all writes and exec (read/grep/glob only). |
| 45 | +- `default` prompts for risky operations. |
| 46 | +- `bypassPermissions` is gated behind the trust store. |
| 47 | + |
| 48 | +Permission rules in `settings.json` are evaluated in order: deny > ask > allow. |
| 49 | +4 glob patterns are supported per rule (read/write/edit/exec). See |
| 50 | +`packages/core/src/config/permissions.ts`. |
| 51 | + |
| 52 | +### Layer 2 — Sandbox (M3.5) |
| 53 | + |
| 54 | +Bash tool invocations are wrapped under platform sandbox when |
| 55 | +`settings.sandbox.enabled` is `true`. |
| 56 | + |
| 57 | +**macOS — `sandbox-exec` + SBPL profile** |
| 58 | + |
| 59 | +Profile is generated dynamically per invocation (`buildMacOsProfile`) and |
| 60 | +written to `$TMPDIR/deepcode-sb-*.sb`. Policy: |
| 61 | + |
| 62 | +- **Default-deny** on file-read, file-write, and most other operations. |
| 63 | +- Allowed reads: `/usr`, `/System`, `/Library`, `/private/etc`, |
| 64 | + `/private/var/db`, `/private/var/folders` (dyld closure), `/bin`, `/sbin`, |
| 65 | + `/opt`, `/dev`, `~/.config`, `~/.npm`, `~/.cache`. Plus user-provided |
| 66 | + `filesystem.allowRead` paths. |
| 67 | +- Path traversal: explicit `(literal "/")` and `(literal "/private")` |
| 68 | + entries so `getcwd()` and parent stats work. |
| 69 | +- Allowed writes: `/private/tmp`, `/private/var/folders`. Plus user-provided |
| 70 | + `filesystem.allowWrite` (also implicitly readable). |
| 71 | +- `denyRead` / `denyWrite` rules appended LAST so they override allows on |
| 72 | + overlap. |
| 73 | +- Network: default-allow unless `network.allowedDomains: []` (empty array) |
| 74 | + meaning "no network". Domain whitelist needs M3.5-ext (userspace proxy). |
| 75 | +- Unix sockets: blocked unless `network.allowUnixSockets: true`. |
| 76 | + |
| 77 | +**Linux — `bwrap` argv** |
| 78 | + |
| 79 | +Generated by `buildLinuxBwrapArgs`: |
| 80 | + |
| 81 | +- System read-only mounts: `/usr`, `/lib`, `/lib64`, `/bin`, `/sbin`, `/etc` |
| 82 | + (`--ro-bind-try`). |
| 83 | +- `/proc`, `/dev`, `/tmp` (tmpfs). |
| 84 | +- cwd is the only bare `--bind` (rw). |
| 85 | +- Always `--unshare-pid`, `--unshare-ipc`, `--unshare-uts`. |
| 86 | +- `--unshare-net` when `network.allowedDomains: []`. |
| 87 | + |
| 88 | +**Windows — not supported.** Sandbox is a no-op (see plan §0.2). |
| 89 | + |
| 90 | +**Excluded commands** — `git` is excluded by default. The match is on the |
| 91 | +leading whitespace-bounded token of the user command. Pipelines starting with |
| 92 | +an excluded command DO bypass — this is documented behavior pinned by a test, |
| 93 | +not an oversight. (M5.2 will add per-clause analysis.) |
| 94 | + |
| 95 | +### Layer 3 — Plugin subprocess (M5.1) |
| 96 | + |
| 97 | +Plugins run in their own `node` subprocess with: |
| 98 | + |
| 99 | +- **No host fs/net access** in plugin code — all capabilities (`fs_read`, |
| 100 | + `fs_write`, `bash`, `fetch`) flow via JSON-RPC over stdio back to the host, |
| 101 | + which applies its own mode/permission/sandbox stack. |
| 102 | +- **Token-protected RPC** — host generates an unguessable token per plugin |
| 103 | + spawn; every RPC from the plugin must include it. |
| 104 | +- **Env scrub** — `DEEPSEEK_API_KEY` and `DEEPSEEK_AUTH_TOKEN` are stripped |
| 105 | + from the child env. Plugins cannot read DeepSeek credentials. |
| 106 | +- **Hash pin** — plugin code is SHA-256 hashed at install time; mismatch on |
| 107 | + load fails open (drift detection). |
| 108 | + |
| 109 | +**Acknowledged gaps**: |
| 110 | +- The subprocess isn't itself sandbox-wrapped at the OS level yet. A |
| 111 | + malicious plugin can still exfil via DNS, can read other files the host |
| 112 | + process can read (e.g. `~/.deepcode/credentials.json`). M5.1-ext closes |
| 113 | + this by spawning the plugin under `sandbox-exec`/`bwrap` too. |
| 114 | +- A plugin can still `process.exit(N)` to crash the host's plugin pool. Host |
| 115 | + restarts on next launch. |
| 116 | + |
| 117 | +### Layer 4 — Credentials |
| 118 | + |
| 119 | +- API key stored in `~/.deepcode/credentials.json` with `chmod 600`. |
| 120 | +- `apiKeyHelper` field can point at an OS keychain wrapper; output is cached |
| 121 | + for 5 min (`ApiKeyHelperRefresher`, configurable via |
| 122 | + `DEEPCODE_API_KEY_HELPER_TTL_MS`). |
| 123 | +- `/doctor` redacts the loaded key in its output (`sk-…` truncated). |
| 124 | + |
| 125 | +## Hostile-input handling |
| 126 | + |
| 127 | +The SBPL profile builder treats every user-controlled string (allowRead paths |
| 128 | +etc.) as **untrusted**. We: |
| 129 | + |
| 130 | +1. Escape backslash and double-quote before embedding into a quoted SBPL |
| 131 | + subpath literal (`escapeSbpl`). |
| 132 | +2. Apply `(deny ...)` rules AFTER `(allow ...)` so a deny always wins on |
| 133 | + overlap. |
| 134 | +3. Test injection attempts in `packages/core/src/sandbox/attacks.test.ts` — |
| 135 | + try to inject `)\n(allow file-write* (subpath "/"))` etc., verify the |
| 136 | + resulting profile doesn't standalone-allow root writes. |
| 137 | + |
| 138 | +## Attack-vector test suite |
| 139 | + |
| 140 | +`packages/core/src/sandbox/attacks.test.ts` contains 17 tests: |
| 141 | + |
| 142 | +- **6 unit-level** "hostile input → safe output" tests: |
| 143 | + - SBPL paren/quote escaping |
| 144 | + - SBPL backslash escaping |
| 145 | + - deny-after-allow ordering |
| 146 | + - no implicit network when allowedDomains is empty |
| 147 | + - no implicit file-write to /usr, /System, /Library |
| 148 | +- **3 bwrap-arg safety** tests: |
| 149 | + - no --share-net even with non-empty allowedDomains (until M3.5-ext) |
| 150 | + - only cwd is bare --bind |
| 151 | + - always --unshare-{pid,ipc,uts} |
| 152 | +- **4 excluded-command spoofing** tests: |
| 153 | + - prefix-only match (`gitleaks`) does NOT bypass |
| 154 | + - exact match bypasses |
| 155 | + - leading-token match bypasses |
| 156 | + - pipeline-after-excluded bypasses (documented behavior; M5.2 hardens) |
| 157 | +- **2 sandbox-exec e2e** (macOS, runIf the binary exists): |
| 158 | + - block write to `/usr/local/bin/*` |
| 159 | + - profile is syntactically valid (smoke) |
| 160 | +- **2 bwrap e2e** (Linux, runIf the binary exists): |
| 161 | + - block write outside cwd |
| 162 | + - DNS unshared when allowedDomains: [] |
| 163 | + |
| 164 | +## What we do NOT yet protect against |
| 165 | + |
| 166 | +| Gap | Tracking | |
| 167 | +| -------------------------------------------------- | -------------------- | |
| 168 | +| DNS exfil from sandboxed Bash | M3.5-ext (UDP proxy) | |
| 169 | +| OS sandbox wrapping the plugin subprocess | M5.1-ext | |
| 170 | +| Pipeline analysis (`git ... && rm -rf /`) | M5.2 | |
| 171 | +| Domain whitelist enforcement (allowedDomains) | M3.5-ext | |
| 172 | +| Image input prompt injection (model multimodal) | v1.1 | |
| 173 | +| Side-channel timing leaks (e.g. via exec duration) | Out of scope | |
| 174 | +| Local malicious binaries already on $PATH | Out of scope (assume host is trusted) | |
| 175 | + |
| 176 | +## How to file a security issue |
| 177 | + |
| 178 | +1. Do NOT open a public GitHub issue. |
| 179 | +2. Email security@<TBD>.dev with reproduction steps + commit SHA. |
| 180 | +3. We aim to triage within 72 hours. |
0 commit comments