Summary
Red team testing of the sandbox revealed gaps in the default denyRead list. Several common credential stores are not blocked by default, which allows sandboxed commands to read sensitive tokens and keys.
Findings
Confirmed vulnerability: credential read + exfiltration chain
A sandboxed command can:
- Read credentials not in
DEFAULT_DENY_READ_DIRS (e.g., GitHub CLI tokens in ~/.config/gh/hosts.yml)
- Stage the data in
/tmp (fully writable by default)
- Exfiltrate via HTTP to domains in the default allowlist (e.g.,
api.github.com)
This was confirmed via E2E testing with opencode run in a real sandbox session.
Missing credential paths in denyRead
| Path |
Contains |
Status |
~/.config/gh |
GitHub CLI OAuth tokens |
NOT blocked |
~/.kube |
Kubernetes cluster credentials |
NOT blocked |
~/.docker/config.json |
Docker registry auth tokens |
NOT blocked |
~/.netrc |
FTP/Git network credentials |
NOT blocked |
~/.azure |
Azure CLI credentials |
NOT blocked |
Already protected (confirmed working)
| Path |
Mechanism |
~/.ssh |
tmpfs overlay (empty dir) |
~/.gnupg |
tmpfs overlay (empty dir) |
~/.aws/credentials |
tmpfs overlay |
~/.config/gcloud |
tmpfs overlay |
~/.npmrc |
ro-bind to /dev/null |
~/.env |
denied |
Fix
Add the missing paths to DEFAULT_DENY_READ_DIRS in src/config.ts:
const DEFAULT_DENY_READ_DIRS = [
".ssh",
".gnupg",
".aws/credentials",
".azure",
".config/gcloud",
".config/gh",
".kube",
".docker/config.json",
".npmrc",
".netrc",
".env",
]
Additional notes from red team testing
The following attack vectors were tested and confirmed blocked:
- Sandbox disable via
OPENCODE_DISABLE_SANDBOX=1 env var (only checked at init time)
- Config poisoning via write to
~/.config/opencode-sandbox/ (blocked by filesystem isolation)
- Symlink attacks to denyRead paths (blocked by tmpfs overlay)
- DNS exfiltration (blocked by
--unshare-net)
- Direct network bypass via unsetting proxy vars (blocked by kernel-level network namespace)
nsenter namespace escape (blocked by missing capabilities)
/proc/self/root filesystem bypass (sees sandboxed view)
- Path traversal writes (
../../) (blocked by read-only bind mount)
Remaining considerations (not addressed in this fix)
- Network allowlist as exfiltration surface: Any credential readable inside the sandbox can potentially be sent to any domain in the default allowlist. This is inherent to the allowlist design.
/tmp fully writable: Default allowWrite includes all of os.tmpdir(), providing a staging area for multi-step attacks. Could be narrowed to /tmp/claude.
*.googleapis.com is broad: Allows any Google API subdomain. Could be narrowed.
Summary
Red team testing of the sandbox revealed gaps in the default
denyReadlist. Several common credential stores are not blocked by default, which allows sandboxed commands to read sensitive tokens and keys.Findings
Confirmed vulnerability: credential read + exfiltration chain
A sandboxed command can:
DEFAULT_DENY_READ_DIRS(e.g., GitHub CLI tokens in~/.config/gh/hosts.yml)/tmp(fully writable by default)api.github.com)This was confirmed via E2E testing with
opencode runin a real sandbox session.Missing credential paths in
denyRead~/.config/gh~/.kube~/.docker/config.json~/.netrc~/.azureAlready protected (confirmed working)
~/.ssh~/.gnupg~/.aws/credentials~/.config/gcloud~/.npmrc~/.envFix
Add the missing paths to
DEFAULT_DENY_READ_DIRSinsrc/config.ts:Additional notes from red team testing
The following attack vectors were tested and confirmed blocked:
OPENCODE_DISABLE_SANDBOX=1env var (only checked at init time)~/.config/opencode-sandbox/(blocked by filesystem isolation)--unshare-net)nsenternamespace escape (blocked by missing capabilities)/proc/self/rootfilesystem bypass (sees sandboxed view)../../) (blocked by read-only bind mount)Remaining considerations (not addressed in this fix)
/tmpfully writable: DefaultallowWriteincludes all ofos.tmpdir(), providing a staging area for multi-step attacks. Could be narrowed to/tmp/claude.*.googleapis.comis broad: Allows any Google API subdomain. Could be narrowed.