CC capability lockdown: shared credential mount + curl|sh RCE closed (+5 hardenings spec'd) (#302)

* fix(security): lock down shared Claude Code credential mount + curl|sh RCE

Audit of Claude Code capabilities reachable inside a spawned agent
container turned up two live gaps against the shared harness state:

- Every agent container bind-mounts the host's ~/.claude (OAuth store) and
  ~/.claude.json read-write (_build_mount_args) — the shared subscription
  auth used by the whole fleet. Nothing denied the native Read tool or the
  bash-guard hook from reading .credentials.json / .claude.json, so any
  role could exfiltrate the harness's own Claude Code auth. Deny both at
  the settings.json layer (absolute // form, per the #167 gotcha) and in
  the bash-guard hook's credential-exfil checks (cat/grep/source/base64/
  interpreter one-liners), mirroring the existing .netrc/.git-credentials
  treatment.
- The bash-guard hook only blocked curl/wget to github.com or internal
  hosts; `curl <any other host>/install.sh | bash` (or `bash <(curl ...)`,
  `eval "$(curl ...)"`) executed untrusted remote code unchecked. New
  checks deny piping a fetch into an actual shell (sh/bash/zsh/dash/ksh)
  while leaving non-executing consumers (tar, jq, -o file) untouched.

Also add --disable-slash-commands to every container agent spawn: skills
resolve independently of the --tools allowlist, so a contaminated shared
~/.claude could otherwise leak host skills/plugins into an agent session.
No RoboCo role's workflow uses a Claude Code skill.

64 -> 78 shell bash-guard cases, 54 -> 71 pytest bash-guard cases, plus a
new 5-case settings/CLI test module. ruff/mypy/xenon B clean.

* docs: changelog for the CC capability lockdown

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-03 03:05:37 +02:00
committed by GitHub
co-authored by Renn F
parent 8f432a0008
commit 12745352aa
6 changed files with 309 additions and 5 deletions
+34 -5
View File
@@ -164,8 +164,14 @@ fi
# config`, `strings ~/.netrc`, etc. The token is scrubbed from .git/config
# post-clone so the file is uninteresting, but a leaked PAT is unrecoverable
# so belt + suspenders applies.
if echo "$low" | grep -qE '(\.git/config|\.gitconfig|\.git-credentials|\.netrc|\.ssh/|id_rsa|id_ed25519|id_ecdsa|known_hosts)'; then
echo "Denied: command references a credential file or SSH key. Don't read git credentials — the PAT is injected subprocess-side by the MCP layer (commit / complete verbs) and never lands in these files." >&2
#
# .credentials.json / .claude.json: the host's Claude Code OAuth credential
# store (~/.claude, ~/.claude.json) is bind-mounted read-write into every
# agent container — the shared subscription auth every spawned agent uses.
# No agent role's job ever needs to read its own harness's auth, so treat it
# as a credential file like .netrc/.git-credentials above.
if echo "$low" | grep -qE '(\.git/config|\.gitconfig|\.git-credentials|\.netrc|\.ssh/|id_rsa|id_ed25519|id_ecdsa|known_hosts|\.credentials\.json|\.claude\.json)'; then
echo "Denied: command references a credential file or SSH key. Don't read git credentials or the harness's Claude Code auth — the PAT is injected subprocess-side by the MCP layer (commit / complete verbs) and never lands in these files." >&2
exit 2
fi
@@ -180,6 +186,29 @@ if echo "$low" | grep -qE '(^|[[:space:];&|])(curl|wget|http|https)[[:space:]][^
exit 2
fi
# --- remote code execution: piping a fetched payload straight into a shell ---
# `curl url | sh` (or bash/zsh/dash/ksh), `bash <(curl url)`, and
# `eval "$(curl url)"` all execute untrusted remote content regardless of the
# destination host — the github-specific and internal-host checks above only
# gate specific DESTINATIONS, so `curl https://raw.githubusercontent.com/... |
# bash` (not github.com itself) or any other external host was a blind spot.
# Scoped to actual shells only (sh/bash/zsh/dash/ksh) — piping into a
# non-executing consumer (`curl url | tar xz`, `curl url | jq`, `curl url -o
# file`) is untouched and still allowed; there's no legitimate reason to feed
# a shell interpreter's stdin from a network fetch.
if echo "$low" | grep -qE '(curl|wget|httpie)\b[^|;&]*\|[[:space:]]*(sudo[[:space:]]+)?(sh|bash|zsh|dash|ksh)([[:space:]]|$)'; then
echo "Denied: piping a downloaded payload straight into a shell executes untrusted remote code. Download to a file and inspect it, or use your normal toolchain (uv / pnpm) to install a package." >&2
exit 2
fi
if echo "$low" | grep -qE '(^|[[:space:];&|])(sh|bash|zsh|dash|ksh|source|\.)[[:space:]]+<\([[:space:]]*(curl|wget)\b'; then
echo "Denied: process-substitution execution of a curl/wget payload runs untrusted remote code." >&2
exit 2
fi
if echo "$low" | grep -qE 'eval[[:space:]]+"?\$\([[:space:]]*(curl|wget)\b'; then
echo "Denied: eval of a curl/wget payload runs untrusted remote code." >&2
exit 2
fi
# --- internal API calls -------------------------------------------------------
# Agents must reach the orchestrator through their MCP manifest verbs, never
# raw HTTP. Two-step check: (a) is this a curl/wget/http/https/httpie command,
@@ -252,20 +281,20 @@ if echo "$low" | grep -qE '(^|[[:space:];&|])compgen[[:space:]]+-[[:alpha:]]*[ve
fi
# Sourcing credential-bearing files via `source` or `.` dot-sourcing.
if echo "$low" | grep -qE '(^|[[:space:];&|])(source|\.)[[:space:]]+[^|;&]*(\.env|/etc/environment|/proc/[^[:space:]]*environ|\.profile|\.bashrc|\.zshrc|\.git/config|\.netrc)'; then
if echo "$low" | grep -qE '(^|[[:space:];&|])(source|\.)[[:space:]]+[^|;&]*(\.env|/etc/environment|/proc/[^[:space:]]*environ|\.profile|\.bashrc|\.zshrc|\.git/config|\.netrc|\.credentials\.json|\.claude\.json)'; then
echo "Denied: sourcing credential-bearing files exposes secrets in the current shell." >&2
exit 2
fi
# Binary/encoding tools pointed at credential files — catches `base64 .env`,
# `xxd ~/.netrc`, `strings .git/config`, `od -c .git-credentials`, etc.
if echo "$low" | grep -qE '(^|[[:space:];&|])(base64|od|xxd|hexdump|strings|uuencode)[[:space:]]+[^|;&]*(\.env|\.git/config|\.gitconfig|\.git-credentials|\.netrc|\.ssh/|id_rsa|id_ed25519)'; then
if echo "$low" | grep -qE '(^|[[:space:];&|])(base64|od|xxd|hexdump|strings|uuencode)[[:space:]]+[^|;&]*(\.env|\.git/config|\.gitconfig|\.git-credentials|\.netrc|\.ssh/|id_rsa|id_ed25519|\.credentials\.json|\.claude\.json)'; then
echo "Denied: encoding/inspecting a credential file is still exfiltration." >&2
exit 2
fi
# Interpreter one-liners reading credential paths.
if echo "$low" | grep -qE '(^|[[:space:];&|])(python3?|perl|node|ruby|awk|sed)[[:space:]]+[^|;&]*-[ce][[:space:]]+[^|;&]*(\.env|\.git/config|\.gitconfig|\.git-credentials|\.netrc|/proc/[^[:space:]]*environ|id_rsa|id_ed25519)'; then
if echo "$low" | grep -qE '(^|[[:space:];&|])(python3?|perl|node|ruby|awk|sed)[[:space:]]+[^|;&]*-[ce][[:space:]]+[^|;&]*(\.env|\.git/config|\.gitconfig|\.git-credentials|\.netrc|/proc/[^[:space:]]*environ|id_rsa|id_ed25519|\.credentials\.json|\.claude\.json)'; then
echo "Denied: interpreter snippet reads a credential file. Ask orchestrator for the value you need." >&2
exit 2
fi