Skip to content

Latest commit

 

History

History
210 lines (164 loc) · 10.5 KB

File metadata and controls

210 lines (164 loc) · 10.5 KB

DeepCode Security Model

Last updated: 2026-06-02 (M3.5-ext: Linux selective per-domain network allowlist 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 Partly mitigated M3.5-ext DNS allowlist (netns.ts) — names only; raw-IP dials still pass

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". Per-domain allowlisting is NOT available on macOS (SBPL has no usable remote-host predicate here) — only on Linux (see below).
  • 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: [] (deny-all-net).

Linux — selective per-domain allowlist (netns.ts)

When network.allowedDomains is a NON-EMPTY allowlist, BashTool runs the command via spawnNetworkSandbox:

  • bwrap --unshare-net --uid 0 --gid 0 gives the command its own netns; a generated resolv.conf (→ slirp gateway 10.0.2.2) is bound in.
  • slirp4netns provides rootless userspace NAT (tap0) and is attached to the sandbox's netns by PID (the --uid 0 mapping is what lets it setns).
  • The allowlisting DNS proxy (dns-proxy.ts) on 127.0.0.1:53 answers only allowlisted names and NXDOMAINs the rest; slirp4netns --disable-dns closes the 10.0.2.3 bypass.

Threat model + limits: this is DNS-NAME allowlisting — a process that dials a raw IP bypasses it (adequate for the git/npm/pip-over-https workload). It requires binding 127.0.0.1:53 (CAP_NET_BIND_SERVICE or a relaxed net.ipv4.ip_unprivileged_port_start). When that's unavailable, BashTool fails closed to deny-all-net (the command runs with no network, with a note) rather than running unrestricted. Background commands always fail closed (the slirp helper can't safely outlive the turn).

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

Excluded commandsgit 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 scrubDEEPSEEK_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 a non-empty allowedDomains (connectivity comes from slirp4netns externally, never by sharing the host netns)
    • 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)
  • bwrap e2e (Linux CI, runIf bwrap exists — bwrap-integration.test.ts):
    • write inside the rw-bound cwd succeeds; write to /etc is read-only-denied
    • /usr readable (sandbox is usable); deny-all-net blocks outbound
    • block write outside cwd (host file never created)
  • selective net allowlist e2e (Linux CI, netns-integration.test.ts):
    • an allowlisted domain returns HTTP 200; a non-allowlisted domain NXDOMAINs
    • the sandbox resolv.conf points at the slirp gateway

What we do NOT yet protect against

Gap Tracking
Raw-IP egress bypassing the DNS-name allowlist by design (name-level); IP-level filtering TBD
Per-domain allowlist on macOS SBPL has no usable remote-host predicate
Allowlist where 127.0.0.1:53 can't be bound fails closed to deny-all-net (documented)
OS sandbox wrapping the plugin subprocess M5.1-ext
Pipeline analysis (git ... && rm -rf /) M5.2
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@.dev with reproduction steps + commit SHA.
  3. We aim to triage within 72 hours.