Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions docs/security-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# DeepCode Security Model

> Last updated: 2026-05-28 (M3.5 hardening + attack-vector test suite landed)

This document is the **single source of truth** for what DeepCode protects
against, what it doesn't, and how each layer composes. If you're reviewing a
PR that touches credentials, sandbox, plugin runtime, or hooks — verify it
against the threat model here.

## Threat model

DeepCode is an LLM-driven coding assistant. The threats we care about, in
decreasing order of operator severity:

| # | Threat | Severity | Where mitigated |
| - | ------------------------------------------------------------------------ | -------- | ----------------------- |
| 1 | Model exfiltrates DeepSeek API key (or other env secrets) via tool call | High | M3.5 sandbox + M5.1 env strip |
| 2 | Model writes arbitrary files outside the project (`/usr/bin`, `/etc`) | High | M3.5 sandbox + permissions |
| 3 | Plugin (third-party code) does either #1 or #2 | High | M5.1 subprocess + (M5.1-ext) OS sandbox |
| 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 |
| 5 | Hostile `settings.json` field (e.g. allowRead path) injects sandbox rule | Medium | escapeSbpl() |
| 6 | Untrusted project's AGENTS.md drives the agent into harmful action | Low | Trust store (`/trust`) |
| 7 | DNS exfiltration of secrets from sandboxed Bash | Acknowledged limitation | M3.5-ext userspace proxy |

## Defence layers

### Layer 0 — Trust store

First time DeepCode opens a folder, you're asked **"Do you trust this
directory?"**. If you say no, the agent runs in a heavily restricted mode:
no exec, no writes outside the project, no `bypassPermissions` mode allowed.

Decisions persist in `~/.deepcode/trust.json`.

### Layer 1 — Mode + Permissions

Every tool call goes through:

```
Mode policy → Permission rules → Sandbox wrap → Exec
```

Modes (`default` | `acceptEdits` | `plan` | `auto` | `dontAsk` | `bypassPermissions`):
- `plan` blocks all writes and exec (read/grep/glob only).
- `default` prompts for risky operations.
- `bypassPermissions` is gated behind the trust store.

Permission rules in `settings.json` are evaluated in order: deny > ask > allow.
4 glob patterns are supported per rule (read/write/edit/exec). See
`packages/core/src/config/permissions.ts`.

### Layer 2 — Sandbox (M3.5)

Bash tool invocations are wrapped under platform sandbox when
`settings.sandbox.enabled` is `true`.

**macOS — `sandbox-exec` + SBPL profile**

Profile is generated dynamically per invocation (`buildMacOsProfile`) and
written to `$TMPDIR/deepcode-sb-*.sb`. Policy:

- **Default-deny** on file-read, file-write, and most other operations.
- Allowed reads: `/usr`, `/System`, `/Library`, `/private/etc`,
`/private/var/db`, `/private/var/folders` (dyld closure), `/bin`, `/sbin`,
`/opt`, `/dev`, `~/.config`, `~/.npm`, `~/.cache`. Plus user-provided
`filesystem.allowRead` paths.
- Path traversal: explicit `(literal "/")` and `(literal "/private")`
entries so `getcwd()` and parent stats work.
- Allowed writes: `/private/tmp`, `/private/var/folders`. Plus user-provided
`filesystem.allowWrite` (also implicitly readable).
- `denyRead` / `denyWrite` rules appended LAST so they override allows on
overlap.
- Network: default-allow unless `network.allowedDomains: []` (empty array)
meaning "no network". Domain whitelist needs M3.5-ext (userspace proxy).
- Unix sockets: blocked unless `network.allowUnixSockets: true`.

**Linux — `bwrap` argv**

Generated by `buildLinuxBwrapArgs`:

- System read-only mounts: `/usr`, `/lib`, `/lib64`, `/bin`, `/sbin`, `/etc`
(`--ro-bind-try`).
- `/proc`, `/dev`, `/tmp` (tmpfs).
- cwd is the only bare `--bind` (rw).
- Always `--unshare-pid`, `--unshare-ipc`, `--unshare-uts`.
- `--unshare-net` when `network.allowedDomains: []`.

**Windows — not supported.** Sandbox is a no-op (see plan §0.2).

**Excluded commands** — `git` is excluded by default. The match is on the
leading whitespace-bounded token of the user command. Pipelines starting with
an excluded command DO bypass — this is documented behavior pinned by a test,
not an oversight. (M5.2 will add per-clause analysis.)

### Layer 3 — Plugin subprocess (M5.1)

Plugins run in their own `node` subprocess with:

- **No host fs/net access** in plugin code — all capabilities (`fs_read`,
`fs_write`, `bash`, `fetch`) flow via JSON-RPC over stdio back to the host,
which applies its own mode/permission/sandbox stack.
- **Token-protected RPC** — host generates an unguessable token per plugin
spawn; every RPC from the plugin must include it.
- **Env scrub** — `DEEPSEEK_API_KEY` and `DEEPSEEK_AUTH_TOKEN` are stripped
from the child env. Plugins cannot read DeepSeek credentials.
- **Hash pin** — plugin code is SHA-256 hashed at install time; mismatch on
load fails open (drift detection).

**Acknowledged gaps**:
- The subprocess isn't itself sandbox-wrapped at the OS level yet. A
malicious plugin can still exfil via DNS, can read other files the host
process can read (e.g. `~/.deepcode/credentials.json`). M5.1-ext closes
this by spawning the plugin under `sandbox-exec`/`bwrap` too.
- A plugin can still `process.exit(N)` to crash the host's plugin pool. Host
restarts on next launch.

### Layer 4 — Credentials

- API key stored in `~/.deepcode/credentials.json` with `chmod 600`.
- `apiKeyHelper` field can point at an OS keychain wrapper; output is cached
for 5 min (`ApiKeyHelperRefresher`, configurable via
`DEEPCODE_API_KEY_HELPER_TTL_MS`).
- `/doctor` redacts the loaded key in its output (`sk-…` truncated).

## Hostile-input handling

The SBPL profile builder treats every user-controlled string (allowRead paths
etc.) as **untrusted**. We:

1. Escape backslash and double-quote before embedding into a quoted SBPL
subpath literal (`escapeSbpl`).
2. Apply `(deny ...)` rules AFTER `(allow ...)` so a deny always wins on
overlap.
3. Test injection attempts in `packages/core/src/sandbox/attacks.test.ts` —
try to inject `)\n(allow file-write* (subpath "/"))` etc., verify the
resulting profile doesn't standalone-allow root writes.

## Attack-vector test suite

`packages/core/src/sandbox/attacks.test.ts` contains 17 tests:

- **6 unit-level** "hostile input → safe output" tests:
- SBPL paren/quote escaping
- SBPL backslash escaping
- deny-after-allow ordering
- no implicit network when allowedDomains is empty
- no implicit file-write to /usr, /System, /Library
- **3 bwrap-arg safety** tests:
- no --share-net even with non-empty allowedDomains (until M3.5-ext)
- only cwd is bare --bind
- always --unshare-{pid,ipc,uts}
- **4 excluded-command spoofing** tests:
- prefix-only match (`gitleaks`) does NOT bypass
- exact match bypasses
- leading-token match bypasses
- pipeline-after-excluded bypasses (documented behavior; M5.2 hardens)
- **2 sandbox-exec e2e** (macOS, runIf the binary exists):
- block write to `/usr/local/bin/*`
- profile is syntactically valid (smoke)
- **2 bwrap e2e** (Linux, runIf the binary exists):
- block write outside cwd
- DNS unshared when allowedDomains: []

## What we do NOT yet protect against

| Gap | Tracking |
| -------------------------------------------------- | -------------------- |
| DNS exfil from sandboxed Bash | M3.5-ext (UDP proxy) |
| OS sandbox wrapping the plugin subprocess | M5.1-ext |
| Pipeline analysis (`git ... && rm -rf /`) | M5.2 |
| Domain whitelist enforcement (allowedDomains) | M3.5-ext |
| Image input prompt injection (model multimodal) | v1.1 |
| Side-channel timing leaks (e.g. via exec duration) | Out of scope |
| Local malicious binaries already on $PATH | Out of scope (assume host is trusted) |

## How to file a security issue

1. Do NOT open a public GitHub issue.
2. Email security@<TBD>.dev with reproduction steps + commit SHA.
3. We aim to triage within 72 hours.
Loading
Loading