6.2 文件系统与网络边界


6.2 文件系统与网络边界

本节摘要:无论 6.1 选了哪一档,落地时调得最多的总是同样两面墙:文件系统与网络。文件系统侧的原则是"根只读、工作区可写、敏感区掩码"——根文件系统以只读方式挂载(写时复制 / overlay 思路提供可写层),可写范围收敛到工作区白名单,判定靠"路径先规范化再判界"(复用 5.1 节的路径作用域),并单独划出掩码区(.git、.env)防改历史与泄密。网络侧的原则是"默认禁网、按域名放行"——大部分编码任务根本不需要出网,需要的只是包 registry 与少数 API 端点;实现上有 DNS 层(粗、可被直连 IP 绕过)与强制代理层(细、可审计)之分,白名单必须在防火墙层兜底。本节给出 fs_net.py 判界代码,它是第 10.3 节 mini-harness 简易沙箱的直接原料。

学习目标

阅读完本节,你应当能够:

  1. 写出"ro 挂载 + 工作区白名单 + 掩码区"的文件系统边界配置。
  2. 实现防穿越与防符号链接的路径判界函数。
  3. 设计出网白名单并指出 DNS 层方案的绕过面。
  4. 说明网络边界与 5.3 节外传闸门的分工。

一、文件系统边界:根只读,工作区可写

三条规则从上到下收紧:

规则 做法 防住什么
根只读(ro 挂载) 整个根文件系统以只读方式呈现;需要写的地方用写时复制(overlay 思路)单独开可写层 对系统文件、其他项目、全局配置的任意写
工作区白名单 可写范围收敛到工作区目录;判定用"路径先 resolve 成绝对路径再判界" workspace/../../.ssh 式穿越(5.1 节三陷阱之一)
掩码区(deny 优先) .git(防改写历史)、.env 与密钥文件(防泄密)、harness 自身的配置目录——即使在工作区内也直接不可见 模型顺手改历史 / 把凭证读进上下文
根文件系统(只读) ├── usr / bin / ... # 只读:系统完好 ├── workspace/ # 可写:唯一的作业区 │ ├── src/ ... # 正常读写 │ └── .git/ # 掩码:deny 优先(在工作区内也不可见) └── .mini_harness/ # 掩码:harness 自身状态(6.3 节理由)

💡 掩码区是"deny 优先"在沙箱里的投影:5.1 节的 deny 表达政策("任何时候不得触碰"),沙箱把它做成物理不可见而不是"判定时拦一下"——判定的执行点可能漏,挂载点不会。

二、路径判界:fs_net.py 的文件系统半边

# fs_net.py —— 文件系统与网络边界示意(写法示意,以实际工程为准) from pathlib import Path WORKSPACE = Path.cwd().resolve() MASKED_NAMES = {".git", ".mini_harness"} # 掩码区:deny 优先 MASKED_PATTERNS = (".env", "id_rsa") # 凭证类:前缀/文件名匹配 def resolve_in_scope(rel: str) -> Path: """路径判界:resolve 消解 .. 与符号链接后,必须仍落在工作区内。""" p = (WORKSPACE / rel).resolve() inside = p == WORKSPACE or WORKSPACE in p.parents masked = any(part in MASKED_NAMES for part in p.parts) or \ p.name.startswith(MASKED_PATTERNS) if not inside or masked: raise PermissionError(f"路径越界或位于掩码区: {rel}") return p

三个实现细节值得点破:

  1. 先 resolve 再判界workspace/../x 与指向墙外的符号链接都在规范化阶段现形——判界必须发生在规范化之后,这正是 5.1 节"路径穿越"陷阱的对策。
  2. 判界在工具层做,不在循环层做:所有文件类工具(read/write/edit)统一过 resolve_in_scope,一处实现全局生效——第 10 章的 in_workspace 就是它的微缩版。
  3. TOCTOU 的诚实边界:判界与实际读写之间存在时间窗,理论上可被竞态利用;容器 / 微 VM 的挂载级隔离没有这个窗口——这是"判定式边界"与"结构式边界"的本质差异,也是升档的真正理由。

三、网络边界:默认禁网,按域名放行

策略 含义 适用
deny 完全禁网 大部分编码任务:读代码、改代码、跑测试都不需要出网
域名白名单 仅放行显式列出的域名 需要装依赖(包 registry)或调 API 的任务
ask 出网前问人 一次性的、无法枚举的下载需求(5.2 节审批流)

出网白名单的粒度是域名而非 IP:域名语义稳定、可读可审计。典型白名单:pypi.org / files.pythonhosted.org(Python 包)、registry.npmjs.org(Node 包)、api.openai.com(模型 API,若沙箱内进程需要)。git push 类远端写操作不在白名单语义内——那是 5.3 节 exfiltrates 闸门(数据外传)的地盘。

实现层有粗细之分:

做法 弱点
DNS 层 劫持解析:非白名单域名解析失败 裸 IP 直连可完全绕过 DNS;DNS rebinding
代理层 强制所有流量过代理(透明代理 / HTTP CONNECT),按域名放行并留审计日志 配置与运维成本;非 HTTP 协议要额外处理
防火墙兜底 出站默认拒绝,仅放行代理与白名单 IP 段 与代理层组合才有意义

⚠️ 常见事故:只做了 DNS 劫持就以为"禁网了"。模型完全可以 curl http://203.0.113.7/... 直连。域名白名单必须配防火墙层的"出站默认拒绝"兜底,否则它只是个提示牌。

四、fs_net.py 的网络半边

# fs_net.py(续)—— 出网白名单判定(写法示意,以实际工程为准) import re NET_ALLOWLIST = { "pypi.org", "files.pythonhosted.org", # Python 包 "registry.npmjs.org", # Node 包 "api.openai.com", # 模型 API } def net_allowed(url: str) -> bool: """按域名精确匹配;裸 IP 直连一律拒绝(防绕过域名白名单)。""" m = re.match(r"https?://([^/:?#]+)", url) if not m: return False # 非 http(s) 不在白名单语义内 host = m.group(1).lower() if re.fullmatch(r"[\d.]+", host): return False # 裸 IP:常见绕过姿势 return host in NET_ALLOWLIST

最后是与 5.3 节的分工,一句口诀:闸门管"数据该不该出去"(语义、可解释、概率性),边界管"流量能不能出去"(物理、结构、零误放)。成熟系统两层都要:闸门拦"看起来要外传"的命令并给出理由,边界保证"没被放行的流量物理上出不去"。

本节要点回顾

  1. 文件系统三规则:根只读(overlay 提供可写层)、工作区白名单(resolve 后判界)、掩码区(deny 优先做成物理不可见)。
  2. 判界三细节:规范化之后才判界、判界做在工具层全局生效、TOCTOU 时间窗是判定式边界的固有弱点(升档的理由)。
  3. 网络三策略:默认 deny、域名白名单、ask;白名单粒度是域名,必须配防火墙出站默认拒绝兜底。
  4. 两层分工:外传闸门管语义、网络边界管物理,缺一不可。

两面墙砌好了,还剩最后一个问题:墙内墙外各住着谁——harness 本体到底该放在沙箱里面还是外面?下一节的答案可能出乎新手意料。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U