Skip to content

web-shell: approval card leaves sibling model-supplied text unsanitised, and the command block comment overclaims coverage #13566

Description

@yiliang114

Deferred from #13549 (merged as 9ac9766), which escaped the managed approval args preview and sanitised the rendered command block. Two review threads on that PR (qwen-code-review-bot, R1-1 / R1-2) were closed by merge rather than by a fix, and nothing else tracks them. All three items below are verified against main at 17347ee.

This is not a regression. Before #13549 the command block rendered command with no sanitizer at all, so that PR strictly narrowed the hole. What is left is (a) a comment that claims more than the code delivers and (b) a bounded set of sibling render sites that are still raw.

1. The command block's comment overclaims coverage

packages/web-shell/client/components/messages/ToolApproval.tsx, just above const commandDisplay = sanitizeControlChars(command ?? ''):

// The command block renders `rawInput.command`, which every producer leaves
// verbatim — so this is the last choke point before an approver reads what
// they are about to authorize. Neutralise invisible controls here (C0/ANSI,
// C1, and the bidi embedding/isolate controls) so a crafted command cannot
// display one string while authorizing different bytes. ...

Neither absolute claim holds:

  • sanitizeControlChars matches /[\x00-\x08\x0b-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g (toolFormatting.ts:117-118) — C0/C1 plus the bidi embeddings/isolates only. It does not match \p{Cf} or U+2028/U+2029, so a command of cat notes.txt followed by astral TAG characters (U+E0020-U+E007F), or by U+200B / U+2060 / U+FEFF / U+00AD / U+061C / U+200E-U+200F / U+2028-U+2029, renders pixel-identically to the clean command in both the <pre> body and its title, while the shell receives the extra bytes. That is exactly "display one string, authorize different bytes".
  • It is not the last choke point: see item 2.

Smallest fix is to narrow the comment to the command string and to the vectors the helper actually neutralises (C0/ANSI, C1, bidi embeddings/isolates — the ones that visibly reorder a command). Widening coverage instead is a separate policy decision, see item 3.

2. Sibling model-supplied render sites in the same dialog are unsanitised

  • .desc — <div className={styles.desc} id={descId} title={descriptionText}>{descriptionText}</div>. getDescriptionText returns rawInput?.description (a real model-supplied run_shell_command parameter) and falls back to request.title. On the daemon/ACP path the emitter builds the title from invocation.getDescription(), and ShellTool.getDescription() returns the command verbatim — so web-shell: the managed approval dialog's primary content path (tool.args) is not bidi/control-character escaped #13517's own payload reaches .desc and reorders visibly one element above the now-sanitised command block, in its tooltip, and in the aria-describedby target a screen reader reads.
  • The exec-warnings <pre> — title={execWarningsText} and body, inside the same isExec && command fragment fix(web-shell): escape control characters in the managed approval args preview #13549 edited. buildOutsideWorkspaceWarning(directory) interpolates the model's raw directory argument and getPermissionContent passes it through verbatim.
  • The showsContent <pre> renders contentText raw for non-managed producers.

Note .desc is referenced by aria-describedby, and the rule stated in the adjacent comment is to only reference ids whose elements actually render — so any sanitising there must keep the whole element gated, not just its text.

3. sanitizeControlChars misses the zero-width / separator set

Widening the shared helper touches every file that already uses it, and it is a policy choice rather than a range search: of the silent-append set, ZWJ, ZWNJ and U+00AD are also legitimate joiners, so escaping \p{Cf} visibly changes legitimate text. Two constraints if it is widened:

  • The replacement is built from ch.charCodeAt(0) (toolFormatting.ts:121-131). Adding \p{Cf} with the u flag without fixing that drops the low surrogate of astral matches — measured, a TAG-appended command becomes "cat notes.txt\udb40\udb40..." instead of "cat notes.txt\udb40\udc72...".
  • \n and \t must stay unescaped: toolFormatting.test.ts:51 pins expect(sanitizeControlChars('a\tb\nc')).toBe('a\tb\nc') and ToolApproval.test.tsx pins the multi-line .join('\n') expectation.

If only the render site should be extended, compose the two existing helpers locally instead of widening the shared one — escapePreviewText (exported from adapters/transcriptAdapter.ts:76) covers \p{Cf} + U+2028/U+2029, sanitizeControlChars covers C0/C1, both emit ASCII, and composition is order-safe and idempotent. For .desc and the warnings <pre> use sanitizeControlChars, not escapePreviewText: the latter's class starts at U+007F and would let ESC/BEL through.

中文说明

从 #13549(已合并为 9ac9766)延期出来的三项。该 PR 转义了托管审批卡片的参数预览,并净化了渲染出的命令块;那两条 review 线程(qwen-code-review-bot R1-1 / R1-2)是被合并关掉的,不是被修掉的,目前没有别的条目在跟踪。以下三项都在 main 17347ee 上核过。

这不是回归:#13549 之前命令块渲染的是完全未处理的 command,所以那个 PR 严格收窄了缺口。剩下的是(a)一段声称超出代码实际覆盖范围的注释,以及(b)一组有界的、仍然原样渲染的同级展示点。

  1. 命令块注释过度声明:sanitizeControlChars 的字符类只有 C0/C1 加 bidi 嵌入/隔离符,不含 \p{Cf} 与 U+2028/U+2029,所以「隐形追加」载荷(TAG U+E0020-U+E007F、U+200B、U+2060、U+FEFF、U+00AD 等)在 <pre> 与其 title 中渲染出的像素与干净命令完全一致,而 shell 收到多出来的字节——正是注释声称不可能的「显示一串、放行另一串」;同时它也不是「最后一道收口点」。最小修法是把注释收窄到命令字符串与该 helper 实际中和的向量。
  2. 同一弹窗内的同级展示点未净化:.desc(rawInput?.description 或回退到 request.title,daemon 路径上 title 来自 ShellTool.getDescription(),即原样命令)、exec 警告 <pre>(buildOutsideWorkspaceWarning(directory) 插值模型原始 directory)、以及 showsContent 分支的 <pre>。注意 .desc 被 aria-describedby 引用,净化时必须保持整个元素的渲染门控。
  3. sanitizeControlChars 缺零宽/分隔符集合:放宽共享 helper 会牵动所有调用方,且本质是策略选择(ZWJ、ZWNJ、U+00AD 同时是合法连字符)。若放宽,必须同一次改掉基于 charCodeAt(0) 的替换(否则星平面匹配会丢低代理项),并保持 \n/\t 不转义(有测试钉住)。若只想扩渲染点,在本地组合 escapePreviewText(sanitizeControlChars(...)) 即可;.desc 与警告两处要用 sanitizeControlChars,因为 escapePreviewText 的字符类从 U+007F 起,会放过 ESC/BEL。

Activity

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

Metadata

Metadata

Assignees

Labels

category/securitySecurity and privacypriority/P2Medium - Moderately impactful, noticeable problemscope/web-shelltype/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