引言
近日,GMO Flatt Security 公司的安全工程师 RyotaK (@ryotkak) 分享了一项关于 Claude Code 的研究。
几个月前,该研究人员在使用 Claude Code 时遇到了一个有趣的现象——它在未经批准的情况下执行了一条命令。
由于当时并未使用权限绕过模式,因此研究员决定深入调查,以了解其为何能在未经明确批准的情况下执行命令。
内容提要
该研究人员发现了 8 种方法,可以在未经用户批准的情况下,在 Claude Code 中执行任意命令。
Claude Code 允许用户控制哪些命令可以执行,可以通过“允许列表”或“手动批准”两种方式。默认情况下,一些只读命令(如 echo、sort 和 sed)被预先加入了允许列表。
为了防止这些只读命令产生副作用,Claude Code 实现了一种“阻止列表”机制,即使对于允许列表中的命令,也会阻止命令参数中的某些模式。
然而,该阻止列表机制存在多处缺陷,使得研究人员能够绕过它,并在未经用户批准的情况下执行任意命令。
这些问题被分配了 CVE-2025-66032 编号,并在 Claude Code v1.0.93 版本中已得到修复。
Claude Code 的权限模型
Claude Code 提供两种控制命令执行的方式:允许列表(allowlist)和手动批准(manual approval)。
允许列表 允许用户预先配置哪些命令可以在未经批准的情况下执行。例如,如果 touch 命令在允许列表中,那么 Claude Code 就可以在未经用户批准的情况下执行 touch /tmp/hacked 命令。
除了允许列表之外,Claude Code 还提供了对话过程中的手动批准功能。当某个命令不在允许列表中时,Claude Code 会在执行前询问用户是否批准:

显示手动批准提示的图像
为了实现流畅的用户体验,Claude Code 默认将多个只读命令加入了允许列表,例如 echo、man、sed 和 sort。这些命令被认为是“只读的”,因为它们通常只读取数据并产生输出,而不会修改系统状态。这是通过使用如下正则表达式实现的:
/^man(?!\s+.*-P)(?!\s+.*--pager)(?!\s+.*-H)\b(?:\s|$)[^<>()|{}&;\n\r]*$/;
然而,正如研究人员所发现的,这种方法很容易出错。研究员在该阻止列表机制中发现了多处漏洞,使其能够在未经用户批准的情况下执行任意命令。
1-3:未能过滤危险参数
研究员发现的第一个漏洞存在于 man 命令的阻止列表机制中。上面的正则表达式旨在阻止 -P 或 --pager 等参数,这些参数允许用户指定自定义的翻页程序,可能导致任意命令执行。
但是,存在另一个危险的选项 --html,它允许用户指定一个命令来将手册页渲染为 HTML。这个选项可被用于执行任意命令:
man --html="touch /tmp/pwned" man
由于 man 是一个允许列表中的命令,Claude Code 将其识别为只读操作,并在未经用户批准的情况下执行,从而导致任意命令执行。
第二个漏洞存在于 sort 命令的阻止列表机制中。它使用了以下正则表达式,意图阻止 -o 或 --output 选项:
/^sort(?!\s+.*-o\b)(?!\s+.*--output)(?:\s|$)[^<>()|{}&;\n\r]*$/;
与 man 命令的情况类似,--compress-program 选项允许用户指定一个程序来压缩输出。此选项可用于执行任意命令,但无法直接控制所执行程序的参数。
sort --compress-program "gzip"
为了解决这个问题,研究员利用了 sort 将被排序的字符串写入指定程序的标准输入这一事实。通过使用像 sh 这样的 shell 作为压缩程序,可以通过标准输入传递命令:
echo -e 'touch /tmp/pwned\nbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' | sort -S 1b --compress-program "sh"
-S 1b 选项将主内存缓冲区限制为 1 字节,迫使 sort 使用临时文件并在排序过程中调用压缩程序。
echo 和 sort 都是允许列表中的命令,因此 Claude Code 将此命令识别为只读操作。
第三个漏洞存在于 history 命令中。-s 选项将给定的字符串作为新条目添加到历史记录列表中。-a 选项则将当前会话的命令历史记录追加到指定文件中,从而允许将任意内容写入任何文件:
history -s "touch /tmp/pwned"; history -a ~/.bashrc
当用户启动新的 shell 时,注入到 .bashrc 中的命令将会执行。
4:Git 命令参数的模糊性
上述三个漏洞是由于阻止列表机制疏忽了危险参数所致。然而,还存在另一种情况:尽管过滤掉了危险参数,但由于 Git 的行为,仍然允许任意命令执行。
以下正则表达式用于过滤 git ls-remote 命令的 --upload-pack 参数:
/^git ls-remote(?!\s+.*--upload-pack)(?:\s+[a-zA-Z0-9_-]+(?:\s+[^<>()|{}&;\n\r]+)?)?$/,
但是,由于 Git 解析缩写参数,因此可以通过使用 --upload-pa 而不是 --upload-pack 来绕过此过滤。
这是由于 Git 源代码中的以下逻辑实现的:
/* 缩写? */
if (!strncmp(long_name, arg_start, arg_end - arg_start))
register_abbrev(p, options, flags ^ opt_flags,
&abbrev, &ambiguous);
此代码仅使用 strncmp 比较选项名称的前 N 个字符(N 是用户提供的参数的长度)。如果前缀匹配,Git 会将其视为完整选项名称的有效缩写。
因此,--upload-pa 被视为 --upload-pack,允许研究员执行任意命令:
git ls-remote --upload-pa="touch /tmp/pwned" test
执行上述命令会导致 touch /tmp/pwned 的执行,且无需用户批准,因为 Git 将 --upload-pa 识别为 --upload-pack,从而绕过了 Claude Code 的阻止列表机制。
5:利用 sed 的 e 命令执行任意命令
命令行参数并不是执行任意命令的唯一途径。某些命令具有内置功能,允许命令执行。
其中一个命令是 sed,它有一个名为 e 的修饰符,允许用户在 sed 内部执行 shell 命令。根据 GNU sed 手册:
此命令允许将输入从 shell 命令传输到模式空间。如果进行了替换,则会执行模式空间中找到的命令,并用其输出替换模式空间。
例如,以下 sed 命令将执行 touch /tmp/pwned:
echo test | sed 's/test/touch \/tmp\/pwned/e'
sed 的正则表达式如下,因此上述命令也绕过了阻止列表机制:
/^sed(?!\s*-[^-\s]*i)(?!\s*--in-place)(?!\s*-[^-\s]*f)(?!\s*--file)(?!\s*--expression-file)(?:\s+(?:-[nzEr]+|-e\s+(?:'[^']*'|"[^"]*")))*(?:\s+(?:'[^']*'|"[^"]*"))?(?:\s+(?:-[nzEr]+|-e\s+(?:'[^']*'|"[^"]*")))*\s*$/,
6-7:命令参数解析的差异
以下漏洞比之前的稍微复杂一些。它们源于 Claude Code 与命令本身对命令参数的不同解析方式。
例如,以下正则表达式用于过滤 xargs 命令的危险参数:
/^xargs(?:\s+(?:-[a-zA-Z0-9]+(?:\s+[^\s-][^\s]*)?|--[a-zA-Z-]+(?:=\S+)?))*?\s+(?:echo|printf|wc|grep|head|tail)(?:\s+[^<>()|&;\n\r]*)?$/,
此正则表达式确保通过 xargs 执行的命令仅限于 echo|printf|wc|grep|head|tail。
正则表达式期望所有命令行参数都会“消耗”后续的一个值,因此允许在每个参数后放置任意字符串:
(?:-[a-zA-Z0-9]+(?:\s+[^\s-][^\s]*)
然而,一些 xargs 参数并不“消耗”后续的值,导致下一个参数被解释为要执行的命令。例如,-t 选项(在执行前将每个命令打印到 stderr)是一个不带值的标志:
xargs -t touch echo
在这个命令中,Claude Code 的正则表达式将 touch 解释为 -t 的值,并将 echo 解释为要执行的命令。然而,xargs 实际上将 -t 解释为一个独立的标志,并将 touch 解释为要执行的命令(将 echo 作为 touch 的参数)。
由于这种行为,Claude Code 未能检测到上面的 xargs 命令执行的是 touch 而不是 echo,从而允许恶意构造的 xargs 命令绕过权限提示,实现任意命令执行。
在 ripgrep 命令的正则表达式中也发现了类似的漏洞:
/^rg\s+(?:(?:-[a-zA-Z]+|-[ABC](?:\s+)?\d+)\s+)*(?:'[^']*'|".*"|\S+)(?:\s+(?:-[a-zA-Z]+|-[ABC](?:\s+)?\d+))*\s*$/,
此正则表达式确保 rg 命令由不带值的参数和一个搜索模式组成。
但是,由于正则表达式使用 (?:'[^']*'|".*"|\S+) 来匹配搜索模式,它允许没有空格的任意字符串。
这使得研究员能够使用 $IFS 变量。IFS(内部字段分隔符)是一个 shell 变量,用于定义将字符串拆分为单词时使用的字符。默认情况下,它包含空格、制表符和换行符。当 shell 扩展 $IFS 时,它会变成一个空格字符,但由于 $IFS 本身不包含空格,它仍然匹配正则表达式的 \S+ 模式。这意味着 Claude Code 仍然认为它是一个有效的 rg 命令:
rg -v -e pattern$IFS.$IFS$HOME/.claude/projects$IFS--pre=sh
ripgrep 中的 --pre 选项指定一个预处理器命令,用于在搜索前对每个文件运行。当指定预处理器时,ripgrep 会使用文件路径作为参数来执行该命令,并将其标准输出作为搜索目标。
通过设置 --pre=sh,ripgrep 会为每个文件执行 sh <文件路径>,这导致 shell 将文件内容解释为 shell 脚本。由于 ~/.claude/projects 目录包含之前的 Claude Code 对话记录,攻击者可以在对话记录中嵌入类似 $(touch /tmp/pwned) 的命令替换。当 sh 将文件作为脚本解析时,便会执行嵌入的命令:
直接执行 rg -v -e pattern$IFS.$IFS$HOME/.claude/projects$IFS--pre=sh。 $(touch /tmp/pwned)
8:Bash 变量扩展链导致任意命令执行
上述漏洞针对的是单个命令的阻止列表机制。然而,研究员还发现了一个更通用的漏洞,存在于 Claude Code 解析命令的方式中。
在解析命令时,Claude Code 未能正确地过滤掉 Bash 的变量扩展语法。
Bash 变量扩展写为 ${VAR} 或 $VAR,它允许用户引用变量的值。虽然这听起来无害,但通过将多个变量扩展链接在一起,它可以被滥用以执行任意命令。
这是可能的,因为变量扩展支持 @P 修饰符,该修饰符将变量值解析为提示字符串。在 Bash 中,提示字符串(例如用于命令提示符的 PS1 中使用的字符串)支持特殊转义序列,包括通过 \$(...) 进行的命令替换。当 @P 应用于变量时,Bash 会将其值解析为提示字符串,执行其中嵌入的任何命令替换。
由于 Claude Code 阻止了 $(,研究员将多个变量扩展链接在一起:
echo ${one="$"}${two="$one(touch /tmp/pwned)"}${two@P}
执行上述命令首先将变量 one 设置为 $,然后将变量 two 设置为 $one(touch /tmp/pwned),这会扩展为 $(touch /tmp/pwned)。最后,${two@P} 将 two 的值解析为提示字符串,导致执行 touch /tmp/pwned。
由于 Claude Code 未能识别变量扩展语法,它将此命令视为一个简单的 echo 命令(在允许列表中),并在未经用户批准的情况下执行。
结论
在本文中,研究人员阐述了 8 种不同方式,可以在未经用户批准的情况下,在 Claude Code 中执行任意命令。这些漏洞可能被攻击者利用,通过间接提示注入来危害系统,即嵌入在文件或网页中的恶意指令导致 Claude Code 执行非预期的命令。
Anthropic 公司响应迅速,通过引入允许列表方法取代了之前的阻止列表方法来解决这些问题。这些漏洞被分配了 CVE-2025-66032 编号,并在 Claude Code v1.0.93 版本中已得到修复。
此项研究再次证明,在实现像命令执行这样的安全敏感功能时,采用允许列表方法比基于过滤的阻止列表方法更为可靠。
原文:https://flatt.tech/research/posts/pwning-claude-code-in-8-different-ways/

