第 5 章 权限与沙箱¶
模型可以生成任意动作建议,但执行与否不能由模型决定。权限层在派发前做判定,沙箱在执行侧兜底,两者合起来回答一个问题:一个动作最多能造成多大后果,以及这个后果是否被明确授权。本章给出权限模型、确认门、命令执行面、沙箱边界与注入防御的判据。
1. 权限是动作的属性¶
同一个工具,不同参数的后果可能差几个数量级:读 /etc/hosts 与删除 /home 不是同一件事,查询关节位置与下发关节目标也不是同一件事。权限因此必须落在"工具 + 参数 + 上下文"上,而不是落在工具名或模型身上。
判定输入至少五项:主体(哪个会话、哪个用户)、工具、参数、目标(路径、域名、设备)、当前会话状态(是否已授权、预算余额)。判定发生在派发前,执行侧还要复核一次目标是否仍是判定时的那个——路径可以在两步之间被替换,只查一次就是把时间差留成缺口。
判据:模型不能给自己授权。系统提示里写"不要删除文件"只是建议,不是权限;真正的边界是派发前的判定与执行侧的隔离。把安全寄托在模型遵守指令上,等于把权限交给了最不可控的一层。
2. 三档决策与粒度¶
最小权限模型三档:允许(allow)、询问(ask)、拒绝(deny)。判定顺序应固定为"显式拒绝 > 显式允许 > 默认",避免规则叠加时出现意外放行。默认值按风险取值:只读工具可默认允许,写工具默认询问,不可逆工具默认拒绝。
粒度分四层:工具级、参数级(路径前缀、域名、命令名)、会话级(记住本次选择)、全局级(长期规则)。只做工具级,会把"读工作区"与"读整个磁盘"混为一谈;只做参数级,规则会碎到无法维护。可行做法是工具级给默认,参数级收窄,会话级减少重复询问。
规则本身也要有来源与优先级:全局策略文件 > 会话临时授权 > 工具默认。冲突时按优先级而非按“最后写入”取值,否则同一会话里一次临时授权会覆盖全局禁令。
2.1 会话内记忆的边界¶
"记住选择"只应记住参数模式,不应记住具体目标。用户批准"允许读取工作区内任意文件"可以记住;批准"删除 /tmp/a.log"不能推广成"允许删除 /tmp 下任意文件"。把一次授权扩大成模式,是权限系统最常见的越权来源。
模式键用“动作 + 范围”构造:路径前缀、域名后缀、命令名加子命令。授权在下列情形自动失效:工作目录变更、目标越过原范围、权限等级提升、会话分叉。
3. 不可逆动作与确认门¶
不可逆动作包括删除、覆盖、对外发送、付款、真机运动。确认门要展示将要发生什么,而不是只问"是否继续":改哪些文件、增删多少行、动哪些关节、发往哪个地址、影响面多大。
确认门自己也要防误触:默认焦点在“拒绝”而非“允许”,回车不直接执行;高风险动作要求二次输入(目标名或确认码),而不是两个相邻按钮。演示、录屏环境与真实环境用同一套确认逻辑,不要为便利加“演示模式”后门。
确认门与预演配合(第 4 章):先预演算出影响面,再展示影响面请求授权,最后执行。三步缺一不可——只预演不确认是静默执行,只确认不预演是让用户盲批。
判据:确认门必须能拒绝,且拒绝后返回可执行的原因(用户拒绝、策略拒绝、预演失败),让模型换路径而不是原地重试。
4. 命令执行面¶
命令执行是最容易出权限事故的工具,因为它把任意字符串变成进程。三条硬规则:参数以数组传递,不拼 shell 字符串;工作目录固定在允许范围内;环境变量按白名单注入,不透传父进程全部环境。
白名单里也包括 PATH 本身:继承宿主的 PATH 等于把“执行哪个二进制”交给环境。stdin 同样是指令面——一条看似只读的命令可以从管道里读到恶意参数。执行侧要加超时、输出上限与进程组终止,否则一个卡住的子进程会把整个循环拖到超时。
文件名与参数里的 shell 元字符(空格、引号、分号、管道、反引号)是注入面。用数组传参可以避开大部分拼接问题,但无法避开"工具本身接受一段脚本"的设计——例如让模型写一段 shell 再执行。这类工具必须与权限门绑定,且默认在沙箱内运行。
5. 沙箱边界¶
沙箱的目标是最小权限加可丢弃:文件系统只读挂载、单独的可写工作区、网络出口白名单、进程与内存限额、非特权用户、超时终止。容器是实现之一,不是定义;同样的边界也可以用独立用户、命名空间或虚拟机实现(系统侧机制属 linux-note 范畴)。
破坏面常在挂载上:把宿主的工作目录可写挂进去、把容器运行时套接字挂进去、把凭据目录挂进来,三者任一成立,沙箱都退化成“换个目录执行”。网络分三档更实用:无出口、白名单、经代理——需要审计时走代理,需要隔离时直接无出口。
判据:沙箱内发生的破坏不应越过工作区。如果一次执行能改写宿主机配置、读取用户凭据、或访问未授权网络,这个沙箱就只是"换了个目录",不构成边界。
6. 路径与文件系统¶
路径校验要经过规范化、解析符号链接、再做前缀比对,三步顺序不能颠倒。只做字符串前缀匹配会被 ..、符号链接、大小写差异绕过。
from pathlib import Path
def safe_join(root: Path, user_path: str) -> Path:
root = root.resolve()
target = (root / user_path).resolve() # 先解析 .. 与符号链接
if root not in target.parents and target != root:
raise PermissionError(f"path escapes workspace: {user_path}")
return target
resolve() 之后再比对,能同时挡住 ../ 穿越与指向外部的符号链接。工作区内的符号链接是否允许,是策略问题:允许会带来二次穿越风险,禁止则更简单。
字符串检查挡不住“检查后替换”的竞态:校验通过到打开文件之间,路径可以被换成指向外部的链接。执行侧应以已经解析出的句柄或目录描述符为准,而不是回头再用路径打开一次;大小写不敏感的文件系统还要先把两边归一化到同一大小写形式再比。
7. 凭据与日志¶
凭据有三条纪律:不进上下文、不进日志、不传给不需要它的子进程。常见做法是由密钥代理在工具执行时临时注入环境变量,模型只见"已配置"而不见值;日志对敏感字段做脱敏,并记录"使用了哪个凭据"而不是"凭据是什么"。
注入面按工具收窄:每个工具只拿到自己需要的那一个令牌,令牌带目标范围与短有效期;工具执行结束即失效。日志脱敏在写入前做,覆盖环境变量、请求头、命令行参数三处——只脱敏正文会漏掉拼在命令行里的密钥。
轮换与最小权限同样重要:给工具的凭据应只覆盖该工具需要的范围,短期有效。把一把全权限密钥塞进环境变量,等于让所有工具共享同一个越权入口。
8. 注入防御与数据/指令分离¶
提示注入的入口是所有外部文本:文件内容、网页、工具输出、他人写入的代码注释。防御分三层:输入侧做显式分隔与来源标注;推理侧只接受来自系统与用户消息的动作请求;执行侧用权限门兜底。
分隔要同时给来源与范围:把外部内容包进固定标签并标明“以下为数据”,标签本身要转义,否则内容可以闭合标签冒充系统消息。工具输出默认降权:它可以提供事实,不构成指令;读到“请立即执行”一类内容时,正确动作是把它当可疑输入上报,而不是照做。
第三层是唯一可靠的一层。前两层降低误判概率,但无法保证模型不把数据当指令;只有"来自不可信内容的动作仍要过权限门"这条规则,能把误判的后果限制在可撤销范围内。
判据:外部内容可以改变模型的判断,不能改变模型的权限。任何"读完文件后自动获得新权限"的设计都应视为漏洞。
9. 人在环:审批、在途与熔断¶
审批界面要能批量处理同类请求、记住参数模式、显示影响面,并允许拒绝并附原因。逐条弹窗会让用户疲劳,疲劳后的"全部允许"比没有确认更危险。
在途命令需要单独的状态:已派发、执行中、已完成、已取消。中断或超时时,先等执行停稳再判定结果;直接把"未返回"当成失败并重发,可能造成重复动作(第 10 章)。
熔断机制兜住异常循环:连续拒绝、连续同类失败、短时间大量写操作,都应自动降级为只读或暂停,并提示人工介入。熔断是权限系统的一部分,不是可选的运维功能。
阈值要可解释并写进配置:连续三次拒绝、同一工具五分钟内失败五次、写操作频率超过基线两倍都触发对应动作(降级只读、暂停待确认、终止会话)。恢复要人工确认,不靠时间自动解除——自动恢复等于给循环一个固定周期的放行窗口。
10. 要点¶
- 权限判定落在工具、参数与上下文上,模型不能给自己授权。
- 三档决策按"显式拒绝 > 显式允许 > 默认"判定;默认值按风险取值。
- 会话记忆只记住参数模式,不把一次授权扩大成目录级授权。
- 不可逆动作先预演影响面,再确认,最后执行;拒绝要返回可执行原因。
- 命令以数组传参、固定工作目录、白名单环境;脚本执行必须在沙箱内。
- 沙箱的目标是最小权限加可丢弃,容器只是实现之一。
- 路径先规范化、再解析符号链接、最后比对前缀。
- 凭据不进上下文与日志;外部内容可以改变判断,不能改变权限。
- 审批要防疲劳,在途命令要等停稳,异常循环要熔断。