tracking: 安全面闭环——已落地机制的绕过(4 项)

Fermée
#192 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

Évaluation

Difficulté
5/5
Temps estimé
Plus d'une semaine
Accessibilité débutants
8/100
Type d'issue
Bug
Clarté
Plutôt claire
Activité
Active
Stack technique
docker, linux, rust
Domaine
backend, security

Piste de recherche

This is a tracking issue for four sandbox and auth bypasses; no code is to be written here. Start from the sub-issues it links (#150, #152, #155, #167) and their cited files, such as src/tools/transport_layer/transport.rs and src/tools/dispatch.rs. Each sub-issue needs its own design and PR, and the cross-cluster file overlaps need coordination first.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

优先级 P0 · 依赖:无 · 类型:tracking(簇设计入口,不产出代码;子单全部落地后关闭)
记号:正文中的「不变量 #N」指 .dev/AGENTS.md 编号,不是 issue 编号;#150 这类才是 issue。本簇子单头部写 depends-on: #N 表示落地串行(同文件/同契约),不会与语义阻塞混淆。

基线 HEAD = 14bac15a(子单同基线)。本单来自一次全仓审计:先按根因归簇,再拆成可独立验收的子单。

共同根因

这四条的共同点是:机制已经落地,但能被绕过,或者声明与执行面不一致——比「功能缺失」更危险,因为运维会以为它生效了。

  1. 凭据面(#150):#89 的清洗只作用于子进程的 env 块(src/tools/transport_layer/transport.rs:803-810 的 scrub_child_env),而 exec_shell 用 Command::new("/bin/sh") 由服务进程直接 spawn(:897-925),shell 的 $PPID 就是 recursive 本身;服务进程自己的环境没有任何清理(非测试代码里 remove_var/prctl/PR_SET_DUMPABLE 零命中),于是 cat /proc/$PPID/environ 就能拿走 #89 明确要拦下的全部 key。注意:remove_var 修不了这个(glibc unsetenv 不改 execve 时的原始副本),必须靠 PR_SET_DUMPABLE=0 或干净环境 re-exec。
  2. 沙箱面(#155):resolve_within_any 只对已存在路径做 canonicalize 复查(src/tools/dispatch.rs:345-374),不存在的路径走词法 starts_with 并原样返回;WriteFile(src/tools/execution/fs.rs:567、648-702)与 EditTool(src/tools/execution/edit.rs:462)不做祖先 canonicalize → 工作区里一个 docs -> /外部 的链接即可写出沙箱。我已实测:同一条链接,写已存在文件被拒、写新文件被放行。
  3. 归属面(#152):#85 给 /sessions/* 建了归属校验(src/http/auth.rs:80-88 may_access_session),但 agui_run(src/http/handlers.rs:2581-2584)与 agui_cancel(:2902-2905)不取身份,线程键只是 blake3(threadId)(src/agui_session.rs:60-63);#147 落地后 load_thread_seed 会把 A 的完整历史播种进 B 的 run(src/http/agui.rs:730),取消也按原始 threadId 索引(handlers.rs:2906-2910)。
  4. 隔离声明面(#167):DockerToolSetProvider 在 Docker 不可用时只 warn 并保留宿主 Bash(src/tools/transport_layer/docker_provider.rs:79-83),sandbox_mode() 仍返回 Container(:88-90);遗留 docker_sandbox.rs 的容器用默认网络、root、无 cap_drop(:42-83),超时不杀 exec(:123-133),Drop 无条件 tokio::spawn(:141-155)。

子单与依赖

子单 依赖 范围
#150 无 启动后 PR_SET_DUMPABLE=0(或干净环境 re-exec)+不再把凭据留在进程环境;补 cat /proc/$PPID/environ 回归
#167 depends-on: #150 Docker 不可用即 fail-closed(或显著降级标志);遗留实现复用 secure_host_config、超时真杀、Drop 用 try_current
#152 无 agui_run/agui_cancel 取 AuthIdentity;线程元数据记 owner/tenant;active_runs 用命名空间化键
#155 无 不存在路径取最长已存在祖先 canonicalize 后复查;返回 canonical 路径;补 O_NOFOLLOW/openat 方向

落地思路

  1. #150 的修法要选对:remove_var 只是减少泄漏面,/proc/pid/environ 仍是 execve 时的副本;主修复是 Linux 启动即 prctl(PR_SET_DUMPABLE, 0)(同 uid 子进程读不到),或启动时用干净环境 re-exec。验收必须在子进程内读 /proc/$PPID/environ,不能只断言 Command::env。
  2. #155 返回 canonical 路径(而非原 abs_joined),缩短 check→write 的 TOCTOU 窗口;单测要覆盖「不存在的新文件」与「已存在的文件」两种,因为现有 tests/invariants/sandbox.rs:71 只覆盖「链接本身就是目标」。
  3. #152 与 #85 的语义保持一致:admin 仍可访问,其余按 (subject, tenant) 严格相等;agui_active_runs 的键必须与线程键同源,否则改名后取消会失效。
  4. #167 首选 fail-closed:宁可不提供 Bash,也不要「报告 Container 却跑在宿主」。
  5. 与 C 簇在 transport.rs、run_background.rs 重叠(C 的 #165/#177 也在 kill/环境路径上),与 B 簇在 handlers.rs 重叠 —— 按全局顺序错开。

簇级验收

  1. 子进程经 RunShell 执行 cat /proc/$PPID/environ,输出不含任一敏感变量名/值;
  2. root/docs -> /tmp/outside 时,Write docs/new.txt(不存在)与 docs/exists.txt(存在)都被拒,沙箱外无文件产生;
  3. 两个不同 subject 的凭据下,B 访问 A 的 threadId(/agui 与 /agui/{id}/cancel)得 403,A 的 transcript.jsonl 未被追加;
  4. Docker 不可用时构建结果不是「宿主 Bash + Container 报告」。

边界

不含默认档多租户数据面互通(#86)、不含 HTTP 侧人审闭环(#88)、不含权限判定绕过(B 簇)——本簇只收「已有机制被绕过」与「声明与执行面不一致」。

实施顺序、PR 拆分与验收矩阵(补充 · 2026-10-10)

波 子单 理由
W1 #150 凭据面影响面最大(默认档即中招),且它要动 transport.rs 的启动/执行路径
W2 #167 同目录(transport_layer/),且 fail-closed 的判断要靠 #150 之后的环境模型稳定下来
并行 #152、#155 分别只动 http/handlers.rs+agui* 与 tools/dispatch.rs,互不冲突,可与上面并行

跨簇文件重叠(必须错开,不能同时派工)

文件 本簇子单 另一簇子单
src/tools/transport_layer/transport.rs #150(启动期加固 + exec_shell 环境) C 簇 #165(process_group 在同一个 exec_shell 里)
src/tasks/run_background.rs #150(宿主档同父进程的证据点) C 簇 #164/#177
src/http/handlers.rs #152(agui_run/agui_cancel) B 簇 #151/#154/#182(同一文件的权限路径)

PR 拆分:一子单一 PR。#150 的 PR 必须先于 C 簇 #165(两者都在 exec_shell 里改 spawn 前的设置,先合 #150 可避免 #165 之后 rebase 冲突)。

簇级验收 → 测试映射

验收 落点
① cat /proc/$PPID/environ 不含敏感变量 tests/issue89_parent_environ.rs(#[cfg(target_os = "linux")],测试进程内先调 harden_process())
② 链接目录写新文件/已存在文件都被拒 tests/invariants/sandbox.rs 新增两例(现有 :71 只覆盖"链接即目标")
③ B 访问 A 的 threadId 得 403 且 A 的 jsonl 未变 src/http/handlers.rs 的 #[cfg(test)] 或 tests/ 新增 HTTP 级用例(两个不同 AuthIdentity)
④ Docker 不可用时不出现"宿主 Bash + Container 报告" src/tools/transport_layer/docker_provider.rs 单测(注入失败的 docker 路径)

共同陷阱

  1. 三处"未归属/未配置"的语义必须与既有实现一致:#85 的规则是"没有归属信息的会话只有 admin 可达"(cold_load.rs:232-236),不要发明第二套规则。
  2. 单用户部署(未启用 auth)不能被打坏:Extension<AuthIdentity> 在 auth 中间件关闭时不存在,#152 必须把"取不到身份"当作"无归属模型、沿用旧行为",而不是 403。
  3. 本簇全部是"机制已存在但被绕过",因此每个 PR 都要包含原始机制仍然生效的对照断言(避免修 A 绕过时把 B 路径关掉)。
Langage dominant
Rust
Étoiles
4
Forks
0
Merge moyen
1 h 2 min
PR mergées (30 j)
5

Préparer son environnement

  • Fournit un Dockerfile ou un fichier Docker Compose
  • Aucun modèle de pull request
  • Aucun guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de jeffkit/recursive

Toutes les issues de jeffkit/recursive

Issues similaires

Plus d'issues Rust

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.