跳到主要内容

安全边界:沙箱 / SSRF / 软恢复

这一章讲 nanobot 怎么把一个能跑 shell、能上网、能改文件的 agent 关在笼子里。它的取舍有个鲜明的特点:边界本身是硬的(不可绕过),但触碰边界不会让 agent 崩溃——拒绝被当成一条「工具错误」喂回模型,让它换个合法思路。

5.0 三道边界,各管一处

边界防什么主文件
工作区路径策略agent 读写到工作区之外nanobot/security/workspace_access.pyworkspace_policy.py
shell 沙箱shell 命令逃逸出工作区 / 碰到敏感目录nanobot/agent/tools/shell.pysandbox.py
SSRF 守卫agent 访问内网/云元数据等私有地址nanobot/security/network.pytools/web.py

5.1 SSRF 守卫:DNS 解析之后再判断

它要解决的小问题

agent 能抓网页、能跑 curl,这就给了它一条「服务端伪造请求」(SSRF)的路:被诱导去访问 http://169.254.169.254(云元数据)、http://127.0.0.1(本机服务)、或内网 IP,泄露凭证或打内网。

关键设计:解析后判 IP,而不是只看字符串

validate_url_target(network.py:61-103)的检查链很扎实:

  1. 只允许 http/https,必须有 hostname。
  2. socket.getaddrinfo 真正解析域名,拿到所有 IP。
  3. 每个解析出的 IP 都过 _is_private 黑名单——只要有一个落在私有段就拒绝。

这一步是重点:只检查 URL 字符串挡不住 evil.com 解析到 127.0.0.1 这种 DNS rebinding。先解析再判 IP 才真正安全。

黑名单 _BLOCKED_NETWORKS(network.py:11-22)覆盖得很全:0.0.0.0/8、私有段(10/8、172.16/12、192.168/16)、回环、169.254/16(链路本地 / 云元数据)、100.64/10(运营商级 NAT)、以及 IPv6 的 ::1fc00::/7fe80::/10

两个容易被绕过的洞,它都堵了

  • IPv6 映射的 IPv4:::ffff:127.0.0.1 在 Python 里是 IPv6Address,既不匹配 127.0.0.0/8 也不匹配 ::1_normalize_addr(network.py:39-51)把它转回 IPv4 形态再判,堵掉这个绕过。
  • 重定向:抓取时跟随重定向可能跳到内网。validate_resolved_url(network.py:106-135)对已 fetch 的 URL(重定向后)再查一次 IP。

想放行内网?显式 whitelist

configure_ssrf_whitelist(network.py:29-36)允许用户通过 tools.ssrfWhitelist 配置 CIDR(例如 Tailscale 的 100.64.0.0/10)来豁免——默认全拦,放行必须显式。还有一个更窄的 allow_loopback,只在「所有解析地址都是回环且 host 字面是 localhost/回环 IP」时才放行(network.py:61-103148-159),给本地预览服务留了条精确的口子。

5.2 shell 沙箱:bwrap 后端

配置了 sandbox: "bwrap" 时,shell 命令会被 wrap_command_bwrap(sandbox.py:14-64)包进一个 bubblewrap 沙箱:

  • 只有工作区被读写挂载;它的父目录(藏着 config.json)被一层新的 tmpfs 盖住——防 agent 读到自己的配置/密钥。
  • media 目录只读挂载(好让命令读到上传的附件)。
  • --new-session --die-with-parent、独立 /proc /dev/tmp 用 tmpfs。

不配 sandbox 时,shell 直接在工作区里跑,隔离退化为「靠 workspace 路径策略 + deny_patterns/allow_patterns 命令模式过滤」(shell.py:54-63179-205)。ExecTool 还内建了一组默认 deny_patterns,并把 SSRF 检查接到命令字符串里(contains_internal_url)。

5.3 软恢复:硬拦截 != 让 agent 崩溃

这是 nanobot 在「安全」上最有辨识度的设计。一个越界/SSRF 拒绝不应该终止整个 agent 运行时——否则用户体验很差,而且 agent 学不到「该换条路」。

AgentRunner._classify_violation(runner.py:1356-1399)把工具错误分类:

  • SSRF 违规:命中 _SSRF_MARKERS,返回原始拒绝 + 一段不可绕过边界说明 _SSRF_BOUNDARY_NOTE(runner.py:1313-1327)——明确告诉模型「别再用 curl/wget/编码 IP/改 DNS/代理重试,去问用户要本地文件或安全公网 URL」。这条作为普通工具结果喂回去,fatal_error 仍是 None,循环继续。
  • 工作区越界:命中 _WORKSPACE_VIOLATION_MARKERS(如 outside the configured workspacepath traversal detected)。同一目标反复触碰会升级提示 repeated_workspace_violation_error(runner.py:1375-1392),逼模型别死磕。

用一句话概括这层哲学:

硬边界(网络/文件系统层真的拒绝)
+
软信号(把「为什么被拒 + 该怎么办」当工具结果喂回模型)
=
agent 不崩、不绕过、还能换合法路径继续

这与「直接抛异常中止 turn」的做法形成对比:边界由 OS/网络层强制保证不可绕过,而恢复策略交给模型在对话里完成

6. 巧妙之处

  • 解析后判 IP。 SSRF 守卫先 getaddrinfo 再查每个 IP,挡住 DNS rebinding 而不只是字符串黑名单(validate_url_target)。
  • IPv6-mapped 归一化。 ::ffff:127.0.0.1 被转回 IPv4 再判,堵一个常见绕过(_normalize_addr)。
  • 父目录被 tmpfs 盖住。 bwrap 沙箱专门挡住 agent 读到自己的 config.json(sandbox.py:48)。
  • 硬拦截、软恢复。 安全墙不可绕过,但拒绝当工具错误喂回,agent 换思路而非崩溃(_classify_violation)。
  • 重复越界升级提示。 同一目标反复撞墙会被升级提醒,避免无意义重试循环(repeated_workspace_violation_error)。

7. 边界与局限(诚实)

  • bwrap 仅 Linux/容器。 _BACKENDS 只有 bwrap 一个后端(sandbox.py:57);macOS/Windows 上没有 OS 级 shell 沙箱,隔离只剩 workspace 路径策略 + 命令模式过滤。
  • 不配 sandbox 时 shell 就在工作区裸跑。 默认部署的隔离强度取决于是否启用沙箱与 deny 模式,用户需自行评估。
  • SSRF 白名单是双刃剑。 ssrfWhitelist 放行的 CIDR 会真的让 agent 能打那段网络,配置错了等于自己开门。

8. 代码地图

主题文件符号
SSRF 校验nanobot/security/network.pyvalidate_url_targetvalidate_resolved_url_is_private_normalize_addr
SSRF 白名单nanobot/security/network.pyconfigure_ssrf_whitelist_BLOCKED_NETWORKS
命令内 URL 检查nanobot/security/network.pycontains_internal_url
bwrap 沙箱nanobot/agent/tools/sandbox.pywrap_command_bwrap_BACKENDS
shell 工具/模式过滤nanobot/agent/tools/shell.pyExecTooldeny_patternsallow_patterns
软恢复分类nanobot/agent/runner.py_classify_violation_SSRF_BOUNDARY_NOTE_is_workspace_violation
工作区策略nanobot/security/workspace_access.pyworkspace_policy.py(路径作用域)