Workspace、Shell 与 SSRF 安全边界

对应源码

安全层源码
Workspace 路径和策略nanobot/security/workspace_access.py、workspace_policy.py
网络目标与 SSRFnanobot/security/network.py
文件、Shell 和 Sandbox Toolnanobot/agent/tools/path_utils.py、filesystem.py、shell.py、sandbox.py
Runner 违规分类nanobot/agent/runner.py::_classify_violation
重复违规反馈nanobot/utils/runtime.py::repeated_workspace_violation_error

学习目标

读完后,你应当能够:

  • 区分 workspace scope、文件路径 guard、shell 命令 guard 和 OS sandbox;
  • 解释 SSRF guard 为什么既检查 URL 又解析 DNS;
  • 判断 Windows 上配置 sandbox: bwrap 的实际结果;
  • 在新增 Tool 时列出必须继承的安全约束。

前置知识

  • 理解绝对路径、相对路径、符号链接和 Path.resolve();
  • 知道 SSRF 是服务端被诱导请求内网或云 metadata 的风险;
  • 知道“模型生成的参数”是不可信输入,即使 system prompt 要求模型守规矩。

1. 安全边界从哪里开始

在 Agent 系统中,下面这些内容都应视为不可信:

  • 用户消息;
  • 网页和文档中的提示注入;
  • 模型生成的 Tool 参数;
  • MCP server 返回的数据;
  • Channel 上传的文件名和 URL;
  • 外部 Provider 的错误文本。

安全不能只写在 prompt 中。nanobot 把关键限制放到工具执行前的确定性代码里。

2. WorkspaceScope 与 ToolWorkspace

nanobot/security/workspace_access.py 定义:

  • WorkspaceScope:一个 Turn 的项目路径、访问模式和 sandbox 状态;
  • WorkspaceScopeResolver:根据入口消息和 Session metadata 解析当前 scope;
  • ToolWorkspace:Tool 执行时看到的有效根目录和限制开关;
  • ContextVar:把当前 Turn 的 scope 传给深层 Tool,而不需要修改每个函数参数。

默认 access mode:

  • restrictToWorkspace=true → restricted
  • restrictToWorkspace=false → full

WebSocket 是可携带项目 scope 的入口;其他 Channel 默认使用配置的 workspace scope。具体分支在 WorkspaceScopeResolver.for_turn() 中。

3. 文件 Tool 的路径约束

文件 Tool 通过:

  • nanobot/agent/tools/path_utils.py::resolve_workspace_path
  • nanobot/security/workspace_policy.py::resolve_allowed_path

完成路径解析和 containment 检查。

允许范围不是简单字符串前缀,而是解析后的目录关系。根据具体 Tool,还可能加入:

  • 只读 media 目录;
  • 明确的额外允许目录;
  • 精确允许的单个文件。

读根和写根可能不同。新增文件类 Tool 时,应复用统一路径 helper,不能自己写 if path.startswith(workspace)。

4. Shell guard 与 sandbox 是两层

ExecTool 在 _prepare_command() 中先做应用层检查:

  1. 解析当前 ToolWorkspace;
  2. 验证 working_dir 没有逃出 workspace;
  3. 用 _guard_command() 检查危险命令、路径和内网 URL;
  4. 构造最小环境变量;
  5. 如配置 sandbox,再包装命令;
  6. 生成平台对应 subprocess。
flowchart TD
    A["模型生成 command"] --> B["working_dir containment"]
    B --> C["deny / allow pattern 与路径 guard"]
    C --> D["SSRF URL guard"]
    D --> E{"配置 sandbox?"}
    E -->|"否"| F["宿主 shell"]
    E -->|"是,Unix"| G["wrap_command"]
    G --> H["bwrap"]
    E -->|"是,Windows"| I["记录警告"]
    I --> F

应用级 guard 能阻止已识别的越界方式,但它不是内核强制隔离。

5. 当前 sandbox 后端

nanobot/agent/tools/sandbox.py 当前只注册:

_BACKENDS = {"bwrap": _bwrap}

Bubblewrap 包装大致提供:

  • workspace 读写 bind mount;
  • media 目录只读;
  • workspace 父目录以 tmpfs 遮蔽,从而隐藏 config.json;
  • /tmp、/proc、/dev 等有限系统视图;
  • --die-with-parent,父进程退出时结束。

在 Windows 上,ExecTool 明确记录:

Sandbox '...' is not supported on Windows; running unsandboxed

随后继续执行未经 bwrap 包装的命令。因此,在 Windows 上不能因为配置里写了 "sandbox": "bwrap" 就声称 shell 已被 OS sandbox。

必须准确表述

restrictToWorkspace 开启且无系统 provider 时,workspace_sandbox_status() 返回 application-level、enforced=False。这是源码对自身安全级别的明确描述。

6. Shell 的其他限制

ExecTool 还实现:

  • 默认 60 秒硬超时;模型单次请求的 timeout 上限 600 秒;
  • config timeout 可设为 0 表示不限制;
  • 输出截断;
  • allow/deny regex;
  • 只转发允许的环境变量;
  • 危险磁盘/删除/关机命令的默认 deny patterns;
  • 禁止直接覆盖内部 memory history/cursor;
  • 取消或超时时终止 subprocess。

这些限制降低风险,但不能穷举所有 shell 语义。真正高风险部署仍应依赖容器、OS sandbox、最小权限用户和网络策略。

7. SSRF guard

validate_url_target() 依次检查:

  1. URL scheme 只能是 HTTP/HTTPS;
  2. 必须有 hostname;
  3. 解析 hostname 的所有地址;
  4. 任一地址命中阻止网段即拒绝。

当前阻止范围包括:

  • loopback;
  • RFC 1918 私网;
  • CGNAT;
  • link-local;
  • 常见云 metadata 地址段;
  • IPv6 loopback、ULA 和 link-local。

IPv4-mapped IPv6 会先归一化,避免 ::ffff:127.0.0.1 绕过 IPv4 blocklist。

重定向目标还会由 validate_resolved_url() 再检查。配置中的 tools.ssrfWhitelist 可加入明确 CIDR,load_config() 会调用 _apply_ssrf_whitelist()。

8. 本地 WebUI 预览的窄例外

代码支持在特定 workspace scope 下访问 literal loopback,用于本地预览。allow_loopback=True 的例外非常窄:

  • hostname 必须是 localhost 或 literal loopback;
  • 所有解析地址都必须是 loopback;
  • 不会因此放行 RFC 1918、link-local、metadata;
  • 公网域名即使解析到 loopback也不被当成合法本地别名。

这比简单的“允许所有内网”安全得多。

9. 凭据与访问控制

学习环境中建议:

  • key 放环境变量,配置只写 ${DEEPSEEK_API_KEY};
  • 不把解析后的 config、环境变量或完整 HTTP header 打进日志;
  • WebUI/Gateway 的 token、pairing 和 Channel allowlist 按官方文档配置;
  • 不把 WebUI 监听地址扩大到公网后仍沿用本机信任假设;
  • MCP HTTP server 当作外部服务审查,stdio command 当作本机代码执行审查。

相关入口:

  • nanobot/pairing/
  • nanobot/gateway/
  • nanobot/channels/websocket.py
  • nanobot/agent/tools/mcp.py
  • nanobot/config/schema.py

10. 新 Tool 的安全清单

新增 Tool 前逐项回答:

  • 参数是否经过 Pydantic/JSON Schema 校验?
  • 文件路径是否用 resolve_workspace_path()?
  • 写操作的允许根是否比读操作更窄?
  • URL 是否用 validate_url_target(),重定向是否再次检查?
  • 是否可能泄漏环境变量、token 或本机路径?
  • 超时与取消能否清理资源?
  • 失败结果是否会诱导模型绕过安全策略?
  • 是否真的可以把 concurrency_safe 设为 True?
  • 是否有安全边界测试,而不只有 happy path?

11. 源码与测试

源码:

  • nanobot/security/workspace_access.py
  • nanobot/security/workspace_policy.py
  • nanobot/security/network.py
  • nanobot/agent/tools/path_utils.py
  • nanobot/agent/tools/filesystem.py
  • nanobot/agent/tools/shell.py
  • nanobot/agent/tools/sandbox.py
  • nanobot/agent/runner.py::_classify_violation
  • nanobot/utils/runtime.py::repeated_workspace_violation_error

测试:

  • tests/security/test_security_network.py
  • tests/security/test_workspace_policy.py
  • tests/security/test_workspace_sandbox.py
  • tests/tools/test_filesystem_tools.py
  • tests/tools/test_exec_platform.py
  • tests/tools/test_exec_security.py
  • tests/tools/test_web_fetch_security.py
  • tests/tools/test_web_fetch_url_sanitization.py

自检

  1. 应用级 workspace guard 与 bwrap 的信任边界有何不同?
  2. 为什么 SSRF 检查不能只匹配 URL 文本中的 127.0.0.1?
  3. Windows 上配置 bwrap 后,实际执行路径是什么?
  4. 为什么 prompt 中写“不要访问私有文件”不能替代路径 guard?

下一篇:打包、WebUI 构建与跨平台