Skip to content

Commit 2d53392

Browse files
oratisclaude
andcommitted
test(core,docs): M3.5 — sandbox attack-vector test suite + security-model.md
Closes the "M3.5: 75% — missing attack vectors" gap from the morning report. · packages/core/src/sandbox/attacks.test.ts (17 tests) - 6 SBPL hostile-input tests: paren/quote/backslash escaping, deny-after-allow ordering, no implicit network when allowedDomains=[], no implicit writes to /usr|/System|/Library. - 3 bwrap arg safety tests: no --share-net, only cwd is bare --bind, pid/ipc/uts always unshared. - 4 excluded-command spoofing tests: prefix-only doesn't bypass; pipeline after-excluded DOES bypass (documented as known M5.2-tracked behavior). - 2 sandbox-exec e2e tests on macOS: block /usr/local/bin write; profile parses without syntax error (smoke). - 2 bwrap e2e tests on Linux: block outside-cwd write; DNS unshared when allowedDomains=[]. · packages/core/src/sandbox/profile.ts hardened so e2e tests pass: - Add `(allow file-read* (literal "/"))` and related (literal "/private") entries so shell getcwd / parent stat succeed under deny-default. - Add `(allow file-read* (subpath "/private/var/folders"))` for dyld closure cache (without this, /bin/sh exited with SIGABRT before any command could run). - Add `(allow process-info*)`. · docs/security-model.md (NEW, ~180 lines): threat model, defence layers (trust → modes → permissions → sandbox → plugin subprocess → credentials), hostile-input handling, attack-vector test inventory, explicit list of known gaps (DNS exfil, OS-wrap of plugin process, pipeline analysis, domain whitelist) with milestone tracking. Tests: 308/10 pass/skip in core (was 293), 41 in cli unchanged. Total 349 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 96ea11a commit 2d53392

3 files changed

Lines changed: 528 additions & 0 deletions

File tree

docs/security-model.md

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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

Comments
 (0)