From 09a234cde365d3988d375ed26006a03a2cd7e8fd Mon Sep 17 00:00:00 2001 From: kewton Date: Tue, 13 Oct 2026 15:40:28 +0900 Subject: [PATCH 1/2] Deny home secret reads in the OS sandbox (Issue #657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #657 (stage 2 of #502) adds an OS read-confinement layer to the #655/#656 write backend: - A shared leaf (src/tools/os_sandbox/secret_locations.rs) holds the home secret list, the #501 workspace credential basenames, and the three #501 path sequences; both backends import it. - macOS Seatbelt denies the home secret locations (subpath params) and the workspace credentials (case-folded regex with a `(/.*)?$` tail so a credential-named directory's contents are denied); a later rule re-allows the strict dotenv templates and a final rule re-denies a template below a credential directory. The regex root is canonicalized. - Linux Landlock handles the read rights (`Execute | ReadFile | ReadDir`, from ABI1) and rewrites the deny list into a read-allow list built once at run start. It canonicalizes each secret location (so a symlinked `~/.ssh` denies its target) and the writable locations (so a symlinked workspace is compared by its real path), and reports in `bash_sandbox_status` when a writable location is the home, an ancestor of a secret, or inside a secret (or when HOME is empty), instead of claiming the read isolation applied. - The read-allow expansion skips symlink entries: `path_beneath_rules` opens a path with `O_PATH`, following the link, so a rule for a symlink entry would attach to its target. A sibling symlink to a secret file, or a secret location that is itself a link, would otherwise open the secret; the real path decides those instead. - The availability probe builds its own write-only ruleset (no allow rules), so its child can still `exec` even though the production ruleset now handles the read set. - The read isolation is reported through the existing `bash_sandbox_status` reason (no new event field, no name/field change). - Docs: SECURITY.md and both cli-reference files describe the read layer, the Linux ReadDir consequence (`ls /`, `ls $HOME`, `echo $HOME/*`), and the home-npm-config consequence. - Tests (added for cargo-mutants gaps): a unit test pins `SandboxPolicy::new(..).home` to the process `HOME` (so emptying `home_dir` is caught), and the workspace-regex test now checks the rule is anchored at the canonical workspace root via the `regex` crate (so emptying `escape_regex`, which would deny the name everywhere, is caught). 読み替え: 本文の受入条件は bash_sandbox_status へ field を足す形を示唆しますが、 tests/issue502_os_sandbox_write.rs が applied + ABI3+ で reason==None、unavailable で reason==プローブ理由、を厳密に固定しており(そのまま通すことが制約)、新しい field を 足すと書き込み隔離の証跡を書き換えることになります。指示に従い、field ではなく既存の reason で読み取りの隔離の適用・未適用を報告しています。 判断: 読み取りは書き込みと同じ backend 可用性で適用されるため、Linux の ABI3+ かつ 完全に閉じている場合は reason を None(=完全に隔離)に保ち、未適用(writable が secret を 覆う場合・secret の中の場合・HOME が空の場合)と ABI<3 の場合に reason を出します。 macOS は write limitation が無いため reason に "read isolation applied" を出します。 Co-authored-by: CommandCodeBot --- SECURITY.md | 89 +- docs/guide/en/cli-reference.md | 49 +- docs/guide/ja/cli-reference.md | 13 +- src/tools/os_sandbox.rs | 203 +++- src/tools/os_sandbox/landlock.rs | 355 ++++++- src/tools/os_sandbox/seatbelt.rs | 104 +- src/tools/os_sandbox/secret_locations.rs | 257 +++++ .../issue502-indirect-read/expectations.toml | 17 + .../fixtures/indirect-cases.jsonl | 6 + .../fixtures/macos-probe-results.md | 62 ++ tests/issue502_os_sandbox_read.rs | 951 ++++++++++++++++++ 11 files changed, 1992 insertions(+), 114 deletions(-) create mode 100644 src/tools/os_sandbox/secret_locations.rs create mode 100644 tests/corpus/apps/issue502-indirect-read/expectations.toml create mode 100644 tests/corpus/apps/issue502-indirect-read/fixtures/indirect-cases.jsonl create mode 100644 tests/corpus/apps/issue502-indirect-read/fixtures/macos-probe-results.md create mode 100644 tests/issue502_os_sandbox_read.rs diff --git a/SECURITY.md b/SECURITY.md index d3faa07c..d6dd1d1d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -15,11 +15,13 @@ execute hostile projects or hostile instructions safely. under the canonical workspace root, including through existing symlinks; newly created workspace symlinks may not target paths outside that root. Lexical inspection cannot prove every indirect effect of an arbitrary - program, so when an OS write sandbox is available it also confines the writes - performed inside a command or script (see [OS Write Sandbox](#os-write-sandbox)). - Bash is not a complete sandbox: shell commands and invoked programs still run - as the current OS user, reads and network are not confined in this stage, and - the sandbox is absent when the backend is unavailable. + program, so when an OS sandbox is available it also confines the writes and + the home-credential reads performed inside a command or script (see + [OS Write and Read Sandbox](#os-write-and-read-sandbox)). Bash is not a + complete sandbox: shell commands and invoked programs still run as the current + OS user, network is not confined in this stage, read confinement covers the + home secret locations rather than every workspace credential on every + platform, and the sandbox is absent when the backend is unavailable. ## `--allow` and `--yes` @@ -49,13 +51,13 @@ predated the run. programs and therefore is not a network sandbox. Network isolation is a later stage of #502 and is independent of the write sandbox below. -## OS Write Sandbox +## OS Write and Read Sandbox -`--sandbox auto|required|off` (default `auto`) adds an OS-level write sandbox at -the two `sh -c` launch points: the model Bash tool (the `bash:verify` interval -reuses the same function) and the verification/acceptance commands. The sandbox -is defense in depth; the lexical checks still run first and can refuse a command -before the shell starts. +`--sandbox auto|required|off` (default `auto`) adds an OS-level write sandbox and +a home-credential read sandbox at the two `sh -c` launch points: the model Bash +tool (the `bash:verify` interval reuses the same function) and the +verification/acceptance commands. The sandbox is defense in depth; the lexical +checks still run first and can refuse a command before the shell starts. - What the OS closes (when the backend is available): a write performed inside a command or script that the lexical guard cannot see — `sh -c 'cp ...'`, @@ -63,11 +65,32 @@ before the shell starts. `sed -i` — and creating a hardlink to an inode outside the workspace. The only writable locations are the workspace, the run's temp directory, the known dependency caches, and device files such as `/dev/null`. -- What remains lexical-only: reads, network access, and every command run while - the backend is unavailable. `--allow bash:verify` and `--offline` are lexical +- Read confinement (Issue #657) denies reading the home secret locations + (`.ssh`, `.aws`, `.gnupg`, `.netrc`, `.git-credentials`, `.npmrc`, `.pypirc`, + `.docker/config.json`, `.kube`, `.config/gh`, `.config/gcloud`) and, on macOS, + a workspace credential named by the #501 rule (`.env`, `.env.*`, `.envrc`, + `.npmrc`, `.pypirc`, `.netrc`, `.git-credentials`, `.ssh`, `service-account*.json`, + `*.private.key`, `*.private.pem`, `*.p12`, `*.pfx`); the three strict dotenv + templates (`.env.example`, `.env.sample`, `.env.template`) stay readable. The + list lives in one leaf module (`src/tools/os_sandbox/secret_locations.rs`) that + both backends import. This closes the reads the lexical guard cannot see: a + credential through a non-secret symlink, a file named by an environment + variable such as `BASH_ENV`, and a name an interpreter assembles at run time. + On Linux only the home secret locations are denied: a workspace credential is + closed lexically for a named reference, and a non-secret symlink to it is not + closed by the read layer (decision 4 of #502). Linux rewrites the deny list as + a read-allow list built once at run start, so listing a directory the run did + not allow fails by design: `ls /`, `ls $HOME`, and `echo $HOME/*` do not work + (the home's siblings are allowed, but `/` and `$HOME` themselves are not + listed). When a writable location (for example the workspace) is the home + itself or an ancestor of a home secret, the write allow list already grants + read on that secret's subtree, so the read layer cannot deny it; the status + then records that the read isolation is not effective instead of claiming it is + applied. An empty `HOME` is reported the same way. +- What remains lexical-only: network access, and every command run while the + backend is unavailable. `--allow bash:verify` and `--offline` are lexical policies, so a command they admit can still start a script; only the sandbox - (when applied) constrains that script's writes. Reads and network are not - confined in this stage. + (when applied) constrains that script's writes and home-secret reads. - Runtime dependency setup runs outside the sandbox: the setup steps that execute `setup.py`, npm `postinstall`/`prepare` hooks, and similar install-time scripts run before or outside the confined `sh -c` launch @@ -90,18 +113,25 @@ before the shell starts. warns once at startup, and records `bash_sandbox_status` with `applied=false` and a reason. `required` instead fails Bash and verifier commands with `bash_sandbox_unavailable`. `off` keeps the previous behavior. None of them - reports "sandboxed" when the sandbox was not applied. + reports "sandboxed" when the sandbox was not applied. The read isolation is + reported through the same event's `reason`: it names the applied read + isolation, or is empty for a fully confined sandbox (reads included). - The dependency caches (cargo registry/git/package-cache, npm, pip, Playwright browsers) stay writable so builds and tests work; they remain a contamination path outside the workspace. On macOS they live under `~/Library/Caches` and `~/.cargo`; on Linux under `~/.cargo`, `~/.npm`, and `~/.cache`. pnpm and yarn - stores are not allowed in this stage. + stores are not allowed in this stage. The home npm config (`~/.npmrc`) is a + read-denied secret, so an `npm` that reads a private-registry `~/.npmrc` fails + inside Bash: that is the intended consequence of not feeding registry tokens to + Bash, not a regression. macOS `mktemp` ignores `TMPDIR` and targets the per-user temp directory, which the sandbox denies. A `mktemp` inside the run temp directory (`mktemp -p "$TMPDIR"` or a template under `$TMPDIR`) succeeds; a bare `mktemp` does not. -The applied-probe record is under -`tests/corpus/apps/issue502-indirect-write/fixtures/macos-probe-results.md`. +The applied write-probe record is under +`tests/corpus/apps/issue502-indirect-write/fixtures/macos-probe-results.md`; the +applied read-probe record is under +`tests/corpus/apps/issue502-indirect-read/fixtures/macos-probe-results.md`. ## Child Process Environment @@ -168,18 +198,21 @@ workspace credentials in this change. registration and before spawn. Because the refusal happens before any read, a rejected call returns an honest -refusal that contains no credential bytes or expansion value. This is not a -claim that no arbitrary Bash program can read a secret: dynamic variables, -indirect scripts, encodings, hardlinks, copies, and TOCTOU are tracked -separately by #502. Keep real secrets outside the agent workspace and edit the -template files instead. +refusal that contains no credential bytes or expansion value. This lexical layer +is not a claim that no arbitrary Bash program can read a secret: dynamic +variables, indirect scripts, encodings, hardlinks, copies, and TOCTOU are why the +OS read sandbox above also denies the home secret locations (and, on macOS, +workspace credentials) at the kernel; the remaining forms are tracked by #502. +Keep real secrets outside the agent workspace and edit the template files +instead. ## Backlog -- OS write sandbox, remaining stages of #502: Linux write confinement (landlock), - read confinement, and network/dependency-install isolation. Stage 1a (macOS - Seatbelt write confinement and the shared leaf module) is implemented here; - the same leaf module is the seam the later stages extend. +- OS sandbox, remaining stage of #502: network and dependency-install isolation. + Stage 1a (macOS Seatbelt write confinement and the shared leaf module), stage + 1b (Linux Landlock write confinement), and stage 2 (home-credential read + confinement on both backends, Issue #657) are implemented here; the same leaf + module is the seam the later stage extends. - Container-sandboxed execution track: run Bash/verifier commands inside a per-workspace container or equivalent OS sandbox with a mounted workspace, minimal environment, and explicit network policy. Superseded in part by the diff --git a/docs/guide/en/cli-reference.md b/docs/guide/en/cli-reference.md index ca1cb51f..b47b7f45 100644 --- a/docs/guide/en/cli-reference.md +++ b/docs/guide/en/cli-reference.md @@ -79,7 +79,7 @@ internal integration surface and is intentionally not a public user flag. | `--style` | `