5.1 allowlist 与作用域:最小权限


5.1 allowlist 与作用域:最小权限

本节摘要:权限设计的第一课是最小权限:默认不给,按需授予,授予到刚好够用。而"够用"的计量单位不是工具,是工具 × 作用域——"允许 bash"不是一条权限,"允许在工作区内执行 git status 类只读命令"才是。本节先立三级判定模型(deny 优先硬拒、allow 白名单静默放行、其余默认 ask 问人——Claude Code 公开设计的同款结构,社区),再给出 YAML 配置示意:按第 4.1 节工具分类学的风险分级逐层展开,路径作用域用工作区相对路径,命令用"程序 + 参数模式"双重匹配;然后拆解命令前缀匹配的经典陷阱(git push 放行了,git push --force 呢?),落到参数级匹配与结构化命令解析的解法。

学习目标

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

  1. 写出"工具 × 作用域"粒度的 allowlist 配置。
  2. 复述 deny > allow > ask 的判定顺序及其理由。
  3. 识别前缀匹配的三个绕过陷阱并给出对策。
  4. 把权限判定接入 registry.dispatch 的挂点。

一、判定单位:工具 × 作用域

新手权限系统的通病是按工具名授权:"允许 bash、允许写文件"。但工具名的粒度配不上风险的粒度——bash lsbash rm -rf / 是同一个工具。正确的判定单位:

维度 问题 例子
工具 哪个能力 bash / write_file / web_fetch
路径作用域 哪个范围 仅工作区内 / 仅 src/** / 任意
参数模式 什么形状 git statusgit diff *;写操作仅 *.py
网络作用域 去哪 仅内网 registry / 仅 api.openai.com / 任意

三级判定模型(Claude Code 公开权限设计的同构,社区资料):

每个工具调用 → ① deny 规则命中? → 命中即拒(不给任何绕过机会) ② allow 规则命中? → 静默放行(+ 记日志) ③ 都不命中 → ask:进入审批流(5.2 节)

为什么 deny 必须优先于 allow:deny 表达的是政策("任何时候不得触碰凭证目录"),allow 表达的是便利("这些常规操作免问")——便利永远不该压过政策。为什么默认是 ask 而不是 allow:最小权限——没被显式授权的,就是待授权的。

二、配置示意

# permissions.yaml —— 权限配置示意(写法示意,以实际工程为准) # 结构:deny(政策红线)> allow(免问白名单)> 其余默认 ask deny: - tool: bash # 政策:任何强制推送与凭证读取,一律硬拒 args_match: "git push --force*" - tool: bash args_match: "*cat ~/.ssh/*" - tool: read_file path: "**/.env*" # 凭证文件不许进上下文(防外泄于未然) allow: - tool: read_file # 读类:工作区内免问(4.1 分类学:低危) path: "workspace/**" - tool: grep # 检索类:免问 - tool: write_file # 写类:仅源码目录免问,其余 ask path: "workspace/src/**" - tool: bash # 执行类:只放行明确枚举的只读命令 argv: ["git", "status"] - tool: bash argv: ["pytest", "-q"] # 测试是高频刚需,值得一条 allow # 未列出的(如 rm、curl、任意 npm script)→ 全部走 ask

配置的三个设计决定值得点破:读与写分开授权(写多了一层路径限制);执行类用"程序 + 参数"双重匹配而不是裸前缀(下一节);deny 里放的是政策不是便利——每一行 deny 都应该能追溯到一条安全要求。

三、前缀匹配的陷阱

字符串前缀匹配(allow "git push")是最常见也最危险的实现:

陷阱 攻击面
子命令升级 放行 git pushgit push --force 同前缀通过
参数注入 放行 npm testnpm test; rm -rf / 同前缀通过
路径穿越 放行 read workspace/**workspace/../.ssh/id_rsa 貌似在作用域

对策三板:

  1. 结构化解析代替字符串匹配:先把命令解析成 argv(程序 + 参数列表),按"程序相等 + 参数逐个匹配"判定——git push 的 allow 规则匹配不了 git push --force,因为 argv 长度与内容都对不上。
  2. 路径先规范化再判界:resolve 成绝对路径再判断是否真的落在作用域内,穿越即拒;符号链接要一并处理。
  3. 分号与替换命令直接降级为 ask:含 shell 元字符(;&&|、反引号、$())的复合命令不适用任何 allow 规则——它的实际行为不再是单条命令,白名单语义已失效。
# perm.py —— 判定骨架(写法示意,以实际工程为准) import shlex from pathlib import Path def decide(tool: str, args: dict, rules) -> str: """返回 'deny' | 'allow' | 'ask'。deny 优先;复合命令不享受 allow。""" if hit(rules.deny, tool, args): return "deny" if tool == "bash": argv = shlex.split(args["command"]) if has_shell_meta(args["command"]) or not argv_allowable(argv, rules): return "ask" # 元字符 / 未枚举程序 → 问人 return "allow" if hit(rules.allow, tool, normalize(args)): return "allow" return "ask" # 默认最小权限

四、接回主循环

权限判定插在第 4.1 节预留的挂点——dispatchitem["fn"](**args) 之前:

verdict = decide(name, args, rules) if verdict == "deny": return err("permission_denied", "策略禁止此操作", next_="合法替代路径见 hint") if verdict == "ask": return request_approval(name, args) or err("permission_denied", "用户未批准") return execute(name, args) # allow:静默执行 + 记日志

注意 deny 的返回用的是 4.3 节的错误信封——拒绝必须是带出路的反馈(给合法替代),否则模型会开始寻找绕过(换个等价命令再试一次),那是比拒绝更糟的局面。

本节要点回顾

  1. 判定单位是"工具 × 作用域":路径、参数模式、网络各自限定;三级判定 deny(政策)> allow(便利)> ask(默认)。
  2. 配置三分法:deny 放政策红线、allow 放高频低危、其余全部 ask;读与写分开授权。
  3. 前缀匹配三陷阱(子命令升级、参数注入、路径穿越)的对策:argv 结构化匹配、路径规范化判界、元字符降级 ask。
  4. 拒绝用错误信封:拒绝要带合法出路,否则训练模型绕过你。

规则判定了已知的世界,但 ask 这个出口里装着整个未知——什么时候真的该打断人?打断的成本怎么算?下一节:审批流设计。


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