Skip to content

splitCompoundCommandSegments splits on an operator inside a trailing # comment #11815

Description

@TianYuan1024

What happened?

splitCompoundCommandSegments does not model # comments, so an operator sitting inside a trailing comment is read as real command structure and the line is split where bash does not split it. The permission decision then asks for confirmation on a command an allow rule already covers.

For echo 'a' # comment ; echo B, bash runs a single command — the comment swallows everything from # to the end of the line. The splitter returns two segments, echo 'a' # comment and echo B, so a user who has allowed Bash(echo *) still gets a prompt.

The direction is fail-closed: the extra segment can only add a rule that must pass, never remove one, so no verdict is weakened. It is a usability defect rather than a security one.

Steps to reproduce

Allow only echo in settings.json:

{
  "permissions": {
    "allow": ["Bash(echo *)"]
  }
}

Then have the model run echo 'a' # comment ; echo B. The call stops for confirmation even though bash runs one echo, which the rule covers.

That bash runs one command is plain from bash itself:

$ bash -xc "echo 'a' # comment ; echo B"
+ echo a
a

Measured shapes

split is what splitCompoundCommand returns; bash is what the shell actually runs.

command bash split
echo 'a' # comment ; echo B 1 2
echo 'a\' # note: use ; carefully 1 2
echo 'a\' # trailing && touch /tmp/x 1 2
echo 'a\' # trailing | touch /tmp/x 1 2
echo hi # nothing here 1 1
git status # don't ; echo B 1 1
echo a#b ; echo B 2 2

The last two are why this is not a one-line change. git status # don't ; echo B happens to come out right only because the apostrophe in don't opens a quote that masks the ;, and echo a#b is correctly left alone because # is only a comment at the start of a word.

What did you expect to happen?

A line that bash runs as one command should be one segment, so an allow rule that covers that command applies without a prompt.

Client information

Client Information

Measured on main at 8af09af29a's merge base, and against the branch of #11765.

Version: 0.23.3
OS: macOS 26.5.2 (arm64)
Node: v22.22.1
Shell: GNU bash 3.2.57(1)-release

Login information

Not relevant — the defect is in permission rule splitting and does not involve the model provider or authentication.

Anything else we need to know?

Filed as the follow-up agreed in review on #11765, which fixes a different defect in the same scanner: a backslash inside a plain '…' string was read as an escape, which swallowed the closing quote and glued a whole line into one segment.

Two of the rows above predate that PR and two are widened by it, and the distinction matters for whoever picks this up:

  • echo 'a' # comment ; echo B over-splits on main today, unchanged by fix(core): read a backslash inside single quotes as literal when splitting #11765.
  • echo 'a\' # … ; … does not over-split on main, but only by accident — the old scanner was still stuck inside an unterminated quote, which masked the comment. Once the quote correctly closes, the ; inside the comment becomes visible and the line splits.

So #11765 widens an existing gap rather than creating one, and narrowing its quote handling to avoid these shapes would re-open the bypass it closes, since the two are the same shape. Comment handling has to be added deliberately instead, and needs to cover at least:

  • # starts a comment only at the start of a word — echo a#b is not a comment.
  • # inside a quoted string is literal.
  • Heredoc bodies, where # is ordinary text.
  • The interaction with the line-continuation handling, since a comment runs to the end of the logical line.

A regression test for each row in the table above would pin the behaviour either way.

中文

发生了什么

splitCompoundCommandSegments 没有建模 # 注释,因此位于行尾注释内部的操作符会被当作真实的命令结构,于是在 bash 并不切分的位置把一行切开。随后的权限判定会对一条授权规则本已覆盖的命令弹出确认。

对 echo 'a' # comment ; echo B 而言,bash 只执行一条命令——注释会吞掉从 # 到行尾的全部内容。切分器却返回两个片段 echo 'a' # comment 和 echo B,于是已经授权了 Bash(echo *) 的用户仍会收到确认提示。

方向是「失效即收紧」的:多出来的片段只会增加一条必须通过的规则,不会减少,因此任何判定都不会被放松。这是一个易用性缺陷,而非安全缺陷。

复现步骤

在 settings.json 中只授权 echo,然后让模型执行 echo 'a' # comment ; echo B。尽管 bash 只执行一条被规则覆盖的 echo,该调用仍会停下来等待确认。

bash 只执行一条命令,从 bash 自身即可看出(见上方英文部分的 console 块)。

实测形态

表格见上方英文部分:split 是 splitCompoundCommand 的返回结果,bash 是 shell 实际执行的命令数。

最后两行说明这不是一处一行就能改好的地方。git status # don't ; echo B 结果正确纯属巧合——don't 里的单引号开启了一个字符串,把那个 ; 挡住了;而 echo a#b 被正确地保持完整,是因为 # 只有位于词首时才是注释。

期望的行为

bash 当作一条命令执行的行,应当是一个片段,这样覆盖该命令的授权规则就能生效而不弹出提示。

客户端信息

见上方英文部分。

登录信息

与本问题无关 —— 该缺陷位于权限规则切分环节,不涉及模型提供方或身份认证。

其他补充信息

这是在 #11765 的评审中约定的后续 issue。那个 PR 修复的是同一个扫描器上的另一个缺陷:普通 '…' 字符串内的反斜杠被当成了转义,从而吞掉闭合引号、把整行粘成一个片段。

上表中有两行早于那个 PR,另有两行是被它扩大的,这个区别对接手的人很重要:

  • echo 'a' # comment ; echo B 在今天的 main 上就会过度切分,fix(core): read a backslash inside single quotes as literal when splitting #11765 未改变这一点。
  • echo 'a\' # … ; … 在 main 上不会过度切分,但纯属巧合 —— 旧扫描器当时仍卡在一个未闭合的引号里,把注释挡住了。一旦引号正确闭合,注释里的 ; 就暴露出来,这一行就被切开了。

因此 #11765 是扩大了一处既有缺口,而不是制造了一个新缺口;而为了规避这些形态去收窄它的引号处理,会重新打开它所关闭的那个绕过,因为两者是同一类形态。注释处理必须作为一项独立工作有意识地加入,至少需要覆盖:

  • # 只有在词首才开启注释 —— echo a#b 不是注释。
  • 引号字符串内部的 # 是字面字符。
  • heredoc 正文,其中 # 是普通文本。
  • 与续行处理的相互作用,因为注释延续到逻辑行的末尾。

为上表每一行补一条回归测试,即可把行为双向钉住。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

category/coreCore engine and logicpriority/P3Low - Minor, cosmetic, nice-to-fix issuesscope/shellShell command executiontype/bugSomething isn't working as expected

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions