feat(sandbox): 三档模式 + fail-closed + 会话级固定(H2) - #359
Conversation
Security review — 2 HIGH ship-blockers; recommend NOT merging until enforcement is wiredThe fail-closed plumbing here is genuinely well done and should be kept: HIGH-1 — session pinning (会话级固定) is recorded but never enforced
Fix: thread the pinned mode into the context that reaches tools — carry HIGH-2 — default mode is
|
按严重度,H2 修的三个洞: **1. write / edit / apply_patch 此前完全不受沙箱约束。** `wrapForSandbox` 唯一调用点是 bash 工具,而三个写工具都是 `path.resolve(ctx.cwd, input.path)`,绝对路径与 `../` 一律照收。也就是说 `LISA_SANDBOX=1` 时 bash 被关进 cwd、write 能写满盘——用户拿到了一个自己 并不拥有的保证。现在两边读同一个 SandboxMode、同一份规格,从结构上不可能 再被限制到不同的根目录。 **2. 非 macOS 静默降级成不受限 `/bin/bash -lc`。** 现在无法强制时抛 `SANDBOX_UNAVAILABLE` 并拒绝执行。"以为沙箱开着其实没开" 比"知道没有沙箱"更危险。Linux 走 bubblewrap(有则用),否则拒绝并给出可 操作的出路(装 bwrap,或显式 `LISA_SANDBOX_MODE=danger-full-access`)。 **3. 权限没有会话级固定。** 模式在会话创建时解析并写进 SessionHeader, 之后改设置不影响已在跑的会话。 三档沿用 dsh 的词汇,不自造: | mode | 文件读 | 文件写 | shell | |---|---|---|---| | read-only | 任意 | 拒绝 | 无可写路径 | | workspace-write | 任意 | 工作区 + 临时目录 | 工作区 + 临时目录 | | danger-full-access(默认) | 任意 | 任意 | 不受限 | 两个刻意的决定,都写进了代码注释: - **读在所有模式下都不设界**。Seatbelt 的策略无条件放行 `file-read*`,若 只把 fs 的读收紧,就重新制造了 H2 要消灭的那种不对称,还会弄坏一切读 工作区之外(含 ~/.lisa)的工具。"能不能读"由上一层的工具子集决定。 - **临时目录在 workspace-write 下默认可写**。同一模式下 bash 本来就能写 /tmp,再去拦 write 是表演不是边界。`allowTemp: false` 仅供测试隔离。 默认值**保持不变**(danger-full-access):本地有人值守的 REPL 与往 shell 里敲命令是同一信任姿态,静默收紧所有人的现有配置不是本次改动的职责。本次 修的是"把沙箱打开时它真的管住文件写"。按 surface 收紧无人值守默认值(计划 §3 的表)是单独一步,会改行为,另开。 实现落在 H1 的 seam 上:sandboxed 提供方包住 local 提供方,`capsOf` 按解析 出的模式选世界——所以 `LISA_SANDBOX=1` 现在自动覆盖全部 fs/shell 工具, 而不只是 bash。符号链接逃逸用"最近存在祖先的 realpath"处理,纯路径检查会 放过它。 测试 27 例:../ 逃逸、绝对路径逃逸、**符号链接穿透**、edit/apply_patch 的 update 与 delete、read-only 连工作区内也拒、读不受影响、临时目录默认可写、 不支持平台 fail-closed、Seatbelt 策略逐档断言、模式解析优先级、拼写错误报错 而非静默回退、会话级固定不被后续设置变更加宽。 本机实测:LISA_SANDBOX=1 下 bash 与 write 同时被拒(此前 write 会成功), 两边错误信息一致指向同一个根目录。全量 1592 通过。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ead) The ship-blocker: header.sandboxMode was written but had zero enforcement consumers — capsOf re-resolved from process.env every call, so an explicit pin did nothing and concurrent sessions couldn't differ. Now: - ToolContext carries sandboxMode; capsOf resolves the world from it (defaultCapabilitiesFor(cwd, ctx.sandboxMode)), falling back to the env default only when nothing is pinned. This is the single point where a pin actually takes effect. Enforcement test added: a read-only pin refuses writes even when LISA_SANDBOX_MODE=danger-full-access (the pin wins over env). - cli threads session.header.sandboxMode → the mode is frozen at session creation and can't be widened mid-session by an env change. - subagent + channel router carry sandboxMode so a subagent can't escape its caller's confinement and an operator can pin channels (untrusted remote input). Follow-ups (documented on the PR): web-turn threading, defaulting the unattended/untrusted surfaces (idle/heartbeat/classification) to workspace-write — deployment-dependent (bwrap) so not flipped blind — task.ts inheritance, and routing shell.exec through the same confinement as run (MED, harmless today — grep only reads, and reads are unbounded in every mode by design). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1e2d3e0 to
f825ecd
Compare
Fix applied — the pin now enforces (rebased onto current
|
…finement, pin propagation (#362) Closes the follow-ups flagged when H2 (#359) merged: - HIGH-2: untrusted/unattended surfaces (channels, idle, heartbeat) now default to a confined `untrustedSurfaceMode()` instead of inheriting the local user's `danger-full-access` — capped at workspace-write where the host can enforce it (macOS Seatbelt / Linux bwrap), honouring a stricter env pin. Where no OS sandbox exists, bounded modes would fail closed and silently break autonomy, so it keeps the env default and warns once (loud "install bwrap" > broken heartbeat); an operator can force fail-closed everywhere with LISA_SANDBOX_MODE. - MED-3: `shell.exec` (argv form) now routes through `wrapArgvForSandbox`, the same fail-closed confinement as `run` — so ONE mode governs both shell halves. Previously grep/exec ran unconfined even under read-only/workspace-write. - Pin propagation: the web turn threads `chat.session.header.sandboxMode` (concurrent web sessions confine independently), and `task` (dispatched subagent) inherits `ctx.sandboxMode` so it can't escape its caller's sandbox. Tests: untrusted-surface mode capping (host-independent), argv pass-through when unconfined, and fail-closed/confine of a bounded argv. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
三个洞,按严重度
1.
write/edit/apply_patch此前完全不受沙箱约束。wrapForSandbox唯一调用点是 bash 工具,而三个写工具都是path.resolve(ctx.cwd, input.path),绝对路径与../一律照收。也就是说LISA_SANDBOX=1时 bash 被关进 cwd、write 能写满盘 —— 用户拿到了一个自己并不拥有的保证。2. 非 macOS 静默降级成不受限
/bin/bash -lc。现在无法强制时抛SANDBOX_UNAVAILABLE并拒绝执行。"以为沙箱开着其实没开"比"知道没有沙箱"更危险。3. 权限没有会话级固定 —— 改设置会影响正在跑的任务。
三档(沿用 dsh 的词汇,不自造)
read-onlyworkspace-writedanger-full-access(默认)两个刻意的决定(都写进了代码注释,不是疏漏):
file-read*;若只把 fs 的读收紧,就重新制造了 H2 要消灭的那种不对称,还会弄坏一切读工作区之外(含~/.lisa)的工具。"能不能读"由上一层的工具子集决定。workspace-write下默认可写。 同一模式下 bash 本来就能写/tmp,再去拦write是表演不是边界。默认值不变
默认仍是
danger-full-access:本地有人值守的 REPL 与往 shell 里敲命令是同一信任姿态,静默收紧所有人的现有配置不是本次改动的职责。本次修的是"把沙箱打开时它真的管住文件写"。按 surface 收紧无人值守默认值(计划 §3 的表:dispatch→
workspace-write、idle/渠道→read-only)会改行为,另开 PR。实现
落在 #358 的 seam 上:sandboxed 提供方包住 local 提供方,
capsOf按解析出的模式选世界 —— 所以LISA_SANDBOX=1现在自动覆盖全部 fs/shell 工具而不只是 bash。符号链接逃逸用"最近存在祖先的 realpath"处理(纯路径检查会放过它)。测试(27 例)
../逃逸 / 绝对路径逃逸 / 符号链接穿透 /edit与apply_patch的 update 与 delete /read-only连工作区内也拒 / 读不受影响 / 临时目录默认可写 / 不支持平台 fail-closed / Seatbelt 策略逐档断言 / 模式解析优先级 / 拼写错误报错而非静默回退 / 会话级固定不被后续设置变更加宽。本机实测(
LISA_SANDBOX=1,目标~/lisa-h2-probe.txt):此前第二行会是
ALLOWED。全量 1592 通过 / 0 失败。与 #357 的交互
两者都改
SessionHeader:#357 加version: 2,本 PR 加可选的sandboxMode。互不冲突(可选字段无需版本位),但types.ts会有一处文本冲突,取并集即可。🤖 Generated with Claude Code