From 3b7cc25220666f31b7ff62cc525929b0c7409634 Mon Sep 17 00:00:00 2001 From: Zhijie Shen Date: Tue, 4 Aug 2026 07:50:23 +0800 Subject: [PATCH 1/2] fix(sandbox): drop ~/.config and ~/.local from auto write-set (#299) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit auto mode bound ~/.config and ~/.local writable inside the sandbox. Both sit on the user's shell $PATH / rc-load path (~/.local/bin, ~/.config/fish, ~/.config/npm, ...), so a sandboxed command could plant an executable or rc backdoor that runs UNSANDBOXED later — a persistence/escape vector widening the per-command blast radius. Drops both from the bwrap AUTO_HOME_DIRS array and the sandbox-exec auto profile. Package caches (~/.npm, ~/.cargo, ~/.cache, ...) which are not on $PATH are kept. Closes #299 --- src/tools/exec/sandbox/bwrap.ts | 8 +++++--- src/tools/exec/sandbox/sandbox-exec.test.ts | 18 ++++++++++++++++++ src/tools/exec/sandbox/sandbox-exec.ts | 8 +++++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/tools/exec/sandbox/bwrap.ts b/src/tools/exec/sandbox/bwrap.ts index e70f159..5a211fa 100644 --- a/src/tools/exec/sandbox/bwrap.ts +++ b/src/tools/exec/sandbox/bwrap.ts @@ -10,11 +10,13 @@ const BWRAP_CANDIDATES = ["/usr/bin/bwrap", "/usr/local/bin/bwrap"]; // Common dev-tooling dot-dirs bound writable in auto mode. Pre-created at // runner construction so bwrap's --bind doesn't fail when a path is absent. -// See docs/design/a7-sandbox-loosen-auto.md. +// ~/.config and ~/.local are intentionally EXCLUDED: they sit on the user's shell +// $PATH / rc-load path (~/.local/bin, ~/.config/fish, ~/.config/npm, ...) so a +// sandboxed command could plant an executable or rc backdoor that runs UNSANDBOXED +// later — a persistence/escape vector. See #299. Package caches below are not on +// $PATH and are safe(r) to expose. See docs/design/a7-sandbox-loosen-auto.md. const AUTO_HOME_DIRS = [ ".cache", - ".config", - ".local", ".npm", ".cargo", ".yarn", diff --git a/src/tools/exec/sandbox/sandbox-exec.test.ts b/src/tools/exec/sandbox/sandbox-exec.test.ts index a87fdca..096ee22 100644 --- a/src/tools/exec/sandbox/sandbox-exec.test.ts +++ b/src/tools/exec/sandbox/sandbox-exec.test.ts @@ -95,6 +95,24 @@ describe.skipIf(!isMacOS)("SandboxExecRunner (macOS only)", () => { expect(result.exitCode).toBe(0); }); + it("blocks writes to ~/.config (persistence vector, #299)", async () => { + const testFile = join(HOME, ".config", `.sandbox-test-${Date.now()}`); + const result = await runner.exec( + `mkdir -p ~/.config && touch "${testFile}" 2>&1; rm -f "${testFile}" 2>/dev/null; exit $?`, + { cwd: process.cwd() }, + ); + expect(result.stderr + result.stdout).toMatch(/permitted|denied/i); + }); + + it("blocks writes to ~/.local (persistence vector, #299)", async () => { + const testFile = join(HOME, ".local", `.sandbox-test-${Date.now()}`); + const result = await runner.exec( + `mkdir -p ~/.local && touch "${testFile}" 2>&1; rm -f "${testFile}" 2>/dev/null; exit $?`, + { cwd: process.cwd() }, + ); + expect(result.stderr + result.stdout).toMatch(/permitted|denied/i); + }); + it("blocks writes to ~/.ssh (credential path)", async () => { const testFile = join(HOME, ".ssh", `.sandbox-test-${Date.now()}`); const result = await runner.exec( diff --git a/src/tools/exec/sandbox/sandbox-exec.ts b/src/tools/exec/sandbox/sandbox-exec.ts index dc65b8f..00ab228 100644 --- a/src/tools/exec/sandbox/sandbox-exec.ts +++ b/src/tools/exec/sandbox/sandbox-exec.ts @@ -113,10 +113,12 @@ function buildAutoProfile(cwd: string, home: string): string { (literal "/dev/urandom") (literal "/dev/random")) -; XDG base directories +; XDG cache and package-manager dot-dirs. ~/.config and ~/.local are intentionally +; NOT writable here: they sit on the shell $PATH / rc-load path (~/.local/bin, +; ~/.config/fish, ~/.config/npm, ...), so a sandboxed command could plant an +; executable or rc backdoor that runs UNSANDBOXED later (persistence/escape). +; See #299. (allow file-write* (subpath "${home}/.cache")) -(allow file-write* (subpath "${home}/.config")) -(allow file-write* (subpath "${home}/.local")) ; Package-manager dot-dirs (allow file-write* (subpath "${home}/.npm")) From 5ca581c1dfe961805fe4912223df1cb3e10d2b54 Mon Sep 17 00:00:00 2001 From: Zhijie Shen Date: Sat, 8 Aug 2026 11:19:01 +0800 Subject: [PATCH 2/2] docs(sandbox): mirror ~/.config/~/.local removal rationale into a7 doc (#299) Review asked for the 'why' to live in the design doc a future contributor reads, not just the code comment. Adds an amendment to a7-sandbox-loosen-auto.md explaining the on-$PATH / rc-load distinction that drove dropping ~/.config and ~/.local, the residual ~/.cargo/bin acceptance, and the lack of a $HOME/.config auto-mode escape hatch. References #299 --- docs/design/a7-sandbox-loosen-auto.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/design/a7-sandbox-loosen-auto.md b/docs/design/a7-sandbox-loosen-auto.md index 47ed545..9773f97 100644 --- a/docs/design/a7-sandbox-loosen-auto.md +++ b/docs/design/a7-sandbox-loosen-auto.md @@ -2,6 +2,10 @@ _Status: Phase 1 Implemented — merged in PR [#148](https://github.com/zjshen14/opencli/pull/148) (2026-05-23). Phase 2 Implemented — merged via [#149](https://github.com/zjshen14/opencli/issues/149) (2026-05-24)._ +> **Amendment — issue [#299](https://github.com/zjshen14/opencli/issues/299) / PR [#305](https://github.com/zjshen14/opencli/pull/305):** `~/.config` and `~/.local` were later REMOVED from the auto write-set. The distinction that matters is not "dot-dir in home" but **"on a path that gets executed or sourced later"** — `~/.local/bin` is on `$PATH` and `~/.config/{fish,npm,...}` is an rc-load location, so a sandboxed write there becomes UNSANDBOXED execution on the user's next shell, a persistence/escape vector that defeats the sandbox. Package caches that are not on `$PATH` (`~/.npm`, `~/.cargo/registry`, `~/.cache`, …) remain writable. (The body below still shows the original Phase-1 set for historical accuracy.) +> +> Residual, deliberately-accepted risk: `~/.cargo/bin` and `~/.yarn/bin` are on `$PATH` for some users, yet `.cargo`/`.yarn` are still bound writable. Excluding just the `bin` subdirectory is the targeted fix if that becomes a real vector. Users who need `~/.config` writes in `auto` currently have no option short of `--sandbox off` (a targeted allowlist knob is the better future answer). + --- ## Problem