Skip to content

fix(cli): reject malformed terminal sequences#5305

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/terminal-sequence-code-separator
Jun 18, 2026
Merged

fix(cli): reject malformed terminal sequences#5305
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/terminal-sequence-code-separator

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Tightens the terminalSequence OSC parser so it rejects malformed input where the numeric OSC code is not immediately followed by a ; separator. Previously, after reading the numeric code the parser began scanning for a terminator straight away, so an input like ESC]9oopsBEL was accepted as a valid OSC 9 sequence (with oops mistaken for the payload and BEL as the terminator). The parser now requires a ; after the code before the payload; inputs such as ESC]9oopsBEL, ESC]9BEL, and ESC]9ESC\ are rejected.

The allowed OSC code list (0, 1, 2, 9, 99, 777) is unchanged — this only tightens the boundary between the numeric code and the payload.

Why it's needed

The parser's own documentation states the expected shape is ESC ] <code> ; <payload> <terminator> and that the code should be followed by ; or a terminator. But it accepted OSC input where the numeric code was followed by arbitrary text, so a malformed sequence like ESC]9oopsBEL was passed through to the terminal as an allowed OSC 9 notification instead of being rejected. This fix makes the parser enforce the documented shape, in line with its "reject invalid input entirely — no partial stripping" policy.

Reviewer Test Plan

How to verify

Repro (before this change): a hook-provided terminalSequence such as ESC]9oopsBEL was parsed as a valid OSC 9 sequence and emitted to the terminal. Expected: it should be rejected (parser returns null). Observed after the fix: the malformed sequence is rejected, while well-formed sequences (e.g. ESC]9;hiBEL) continue to parse.

Verification commands (from packages/cli):

  • npx vitest run src/utils/terminalSequence.test.ts
  • npm run typecheck --workspace=packages/cli
  • npm run lint -- --fix packages/cli/src/utils/terminalSequence.ts packages/cli/src/utils/terminalSequence.test.ts
  • npx prettier --check packages/cli/src/utils/terminalSequence.ts packages/cli/src/utils/terminalSequence.test.ts
  • git diff --check

Evidence (Before & After)

N/A — non-visible logic fix; covered by the unit tests above.

Tested on

OS Status
🍏 macOS ✅ CI
🪟 Windows ✅ CI
🐧 Linux ✅ CI

✅ tested · ⚠️ not tested · N/A

Environment (optional)

Unit tests only (npm workspaces).

Risk & Scope

  • Main risk or tradeoff: Low; scoped to the OSC code/payload boundary check in parseOscSequence. The only behavior change is that OSC codes not followed by ; are now rejected.
  • Not validated / out of scope: No changes to the allowed OSC code list, no unrelated refactors, no public API changes, no UI changes.
  • Breaking changes / migration notes: None. Previously-accepted malformed sequences (code not followed by ;) are now rejected, which is the intended bug fix.

Linked Issues

Fixes #5304

中文说明

此 PR 的作用

收紧 terminalSequence 的 OSC 解析器,使其拒绝数字 OSC 代码后面没有紧跟 ; 分隔符的非法输入。此前解析器读取完数字代码后会立即开始扫描终止符,因此像 ESC]9oopsBEL 这样的输入会被当作合法的 OSC 9 序列接受(oops 被误当作 payload,BEL 被当作终止符)。现在解析器要求代码之后、payload 之前必须有 ;;诸如 ESC]9oopsBELESC]9BELESC]9ESC\ 等输入都会被拒绝。

允许的 OSC 代码列表(0、1、2、9、99、777)保持不变——本次改动只收紧数字代码与 payload 之间的边界。

为什么需要

解析器自身的文档说明期望的形态是 ESC ] <code> ; <payload> <terminator>,并说明代码后应跟随 ; 或终止符。但它却接受了数字代码后跟随任意文本的 OSC 输入,因此像 ESC]9oopsBEL 这样的非法序列会作为合法的 OSC 9 通知被透传到终端,而不是被拒绝。此修复让解析器强制遵循其文档化的形态,符合其"完全拒绝非法输入——不做部分剥离"的策略。

审查者测试计划

如何验证

复现(改动前):像 ESC]9oopsBEL 这样的钩子提供的 terminalSequence 会被解析为合法的 OSC 9 序列并发送到终端。预期:应被拒绝(解析器返回 null)。修复后观察到:非法序列被拒绝,而格式正确的序列(例如 ESC]9;hiBEL)仍能正常解析。

验证命令(在 packages/cli 下执行):

  • npx vitest run src/utils/terminalSequence.test.ts
  • npm run typecheck --workspace=packages/cli
  • npm run lint -- --fix packages/cli/src/utils/terminalSequence.ts packages/cli/src/utils/terminalSequence.test.ts
  • npx prettier --check packages/cli/src/utils/terminalSequence.ts packages/cli/src/utils/terminalSequence.test.ts
  • git diff --check

证据(修改前后对比)

不适用 —— 这是非可视的逻辑修复;已由上述单元测试覆盖。

测试平台

操作系统 状态
🍏 macOS ✅ CI
🪟 Windows ✅ CI
🐧 Linux ✅ CI

✅ 已测试 · ⚠️ 未测试 · 不适用

环境(可选)

仅单元测试(npm workspaces)。

风险与范围

  • 主要风险或权衡:低;范围限定在 parseOscSequence 中 OSC 代码/payload 的边界检查。唯一的行为变化是:代码后未跟 ; 的 OSC 现在会被拒绝。
  • 未验证 / 范围之外:未改动允许的 OSC 代码列表,无无关重构,无公共 API 变更,无 UI 变更。
  • 破坏性变更 / 迁移说明:无。此前被接受的非法序列(代码后未跟 ;)现在会被拒绝,这正是预期的 bug 修复。

关联 Issue

Fixes #5304

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

@tt-a1i
tt-a1i marked this pull request as ready for review June 18, 2026 17:00
@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @tt-a1i, thanks for the PR! The fix itself looks focused and reasonable — just the OSC parser tightening the code/payload boundary. 👍

However, the PR body doesn't follow the pull request template. The template requires these headings:

  • ## What this PR does
  • ## Why it's needed
  • ## Reviewer Test Plan (with How to verify, Evidence (Before & After), Tested on)
  • ## Risk & Scope
  • ## Linked Issues
  • <details> Chinese translation section

Your PR uses ## Summary, ## Test plan, ## Demo, and ## AI Assistance Disclosure instead, and is missing the Reviewer Test Plan, Risk & Scope, Linked Issues sections, and the Chinese translation.

Could you update the PR description to match the template? This helps reviewers (especially maintainers evaluating cross-repo PRs) quickly understand motivation, verify the fix, and assess risk. Once updated, the triage can continue.


中文说明

你好 @tt-a1i,感谢提交 PR!修复本身很聚焦且合理 —— 收紧 OSC 解析器中 code/payload 的分隔符校验。👍

但 PR 正文没有按照 PR 模板 的格式来写。模板要求的标题包括:

  • ## What this PR does(做了什么)
  • ## Why it's needed(为什么需要)
  • ## Reviewer Test Plan(审阅者测试计划,含 How to verifyEvidence (Before & After)Tested on
  • ## Risk & Scope(风险与范围)
  • ## Linked Issues(关联 Issue)
  • <details> 中文翻译

你的 PR 使用了 ## Summary## Test plan## Demo## AI Assistance Disclosure 这些标题,缺少 Reviewer Test Plan、Risk & Scope、Linked Issues 等必填部分,以及中文翻译。

能否按照模板更新一下 PR 描述?这能帮助审阅者(特别是评估外部 PR 的维护者)快速理解动机、验证修复效果和评估风险。更新后 triage 会继续进行。

Qwen Code · qwen3.7-max

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — local build + before/after parser harness

Verified 848a9bc against origin/main (f5761ac) on macOS (Darwin arm64, Node v22.22.2). This is a parser-only change on a real input-validation boundary — hook-supplied terminalSequence flows useAttentionNotifications.ts:124emitTerminalSequenceparseAllowedTerminalSequences before bytes are written to the terminal — so I drove it two ways: the real unit suites, and a before/after harness running the actual parser from both origin/main and the PR head against an adversarial corpus.

1. Unit tests / typecheck / lint (PR code)

Check Result
terminalSequence.test.ts 26 passed (3 new + 23 existing)
consumers: useTerminalNotification, useAttentionNotifications, notificationService 24 passed — no downstream breakage
tsc --noEmit (CLI) ✅ no errors in the changed files
eslint + prettier --check (changed files) ✅ clean
CI on the PR ✅ already green on macOS / Windows / Linux

2. Before/after adversarial corpus (23 inputs through both versions)

Category Cases before → after
Valid (OSC 0/1/2/9/99/777, BEL, ST terminator, empty payload, ;-in-payload, multi-token) 11 accept → accept0 regressions
Malformed: code not followed by ; (incl. #5304's ESC]9oops␇) 6 accept → reject
Already invalid (disallowed code, no numeric code, unterminated, plain text, empty) 6 reject → reject (unchanged)

End-to-end gate, headline case:

emitTerminalSequence("\x1b]9oops\x07")
  before: true   → wrapped + written to the terminal
  after:  false  → rejected, 0 bytes written

Result: 6/6 malformed cases fixed, 11/11 valid preserved, 0 regressions.

Notes for the merge decision

  • Regression-safe by construction. The fix only adds input[position] !== ';', i.e. it rejects when the char after the numeric code isn't ;. Every valid allowlisted OSC sequence has ; there, so no valid input can flip — the corpus confirms this empirically (11/11).
  • Slightly broader than the summary says, and correctly so. It tightens the boundary for all allowlisted codes, not just OSC 9 — e.g. ESC]0title␇ (no separator) is now rejected too. It also drops the stale OSC 0/1/2 can have just a title with ';' comment, which no longer matched behavior.
  • Security relevance. Because the input is hook-controlled, accepting ESC]9<arbitrary>␇ previously let a hook smuggle a payload past the allowlist and have it wrapped + written raw. Tightening the code/payload boundary closes that.

Verdict

Correct, focused, and regression-free from my testing. ✅ No code concerns. The only outstanding item is the triage bot's PR-template formatting request (process, not code).

Verified by maintainer @wenshao: local unit suites + a before/after harness running the compiled parser from both origin/main and the PR head (848a9bc) against a 23-input adversarial corpus.

中文版(点击展开)

维护者验证 —— 本地构建 + 前后对比解析器测试

在 macOS(Darwin arm64,Node v22.22.2)上,针对 origin/mainf5761ac)验证了 848a9bc。这是一个纯解析器改动,且位于真实的输入校验边界上——hook 提供的 terminalSequenceuseAttentionNotifications.ts:124emitTerminalSequenceparseAllowedTerminalSequences 校验后才会把字节写入终端——所以我用两种方式驱动验证:真实单元测试套件,以及一个前后对比的 harness(分别从 origin/main 和 PR head 加载真实解析器,跑同一份对抗性语料)。

1. 单元测试 / 类型检查 / lint(PR 代码)

检查 结果
terminalSequence.test.ts 26 通过(3 新增 + 23 既有)
下游消费方:useTerminalNotificationuseAttentionNotificationsnotificationService 24 通过 —— 无下游破坏
tsc --noEmit(CLI) ✅ 改动文件无类型错误
eslint + prettier --check(改动文件) ✅ 干净
PR 的 CI ✅ macOS / Windows / Linux 三平台已全绿

2. 前后对比对抗性语料(23 个输入分别过两个版本)

类别 数量 before → after
合法(OSC 0/1/2/9/99/777、BEL、ST 终止符、空 payload、payload 内含 ;、多 token) 11 accept → accept —— 0 回归
畸形:code 后没有 ;(含 #5304ESC]9oops␇ 6 accept → reject
本就非法(不在白名单的 code、无数字 code、未终止、纯文本、空串) 6 reject → reject(不变)

端到端门控,头号用例:

emitTerminalSequence("\x1b]9oops\x07")
  before: true   → 被包装并写入终端
  after:  false  → 拒绝,写入 0 字节

结论:6/6 畸形用例修复,11/11 合法用例保留,0 回归。

供合并决策参考

  • 结构上不会回归。 该修复只增加了 input[position] !== ';',即当数字 code 后的字符不是 ; 时拒绝。所有合法的白名单 OSC 序列在该位置都有 ;,因此任何合法输入都不会被翻转——语料也实测印证了这点(11/11)。
  • 比摘要描述的范围略广,且是对的。 它收紧了所有白名单 code 的边界,而不只是 OSC 9——例如 ESC]0title␇(无分隔符)现在也会被拒。同时删掉了那条与行为不再相符的过时注释 OSC 0/1/2 can have just a title with ';'
  • 安全相关性。 由于输入是 hook 可控的,此前接受 ESC]9<任意内容>␇ 会让 hook 把一段 payload 绕过白名单、被包装后原样写入终端。收紧 code/payload 边界堵住了这一点。

结论

从我的测试看:正确、聚焦、无回归。✅ 代码层面无顾虑。唯一未决项是 triage bot 对 PR 模板格式的要求(流程问题,非代码问题)。

维护者 @wenshao 验证:本地单元测试套件 + 一个前后对比 harness(分别从 origin/main 与 PR head 848a9bc 加载编译后的解析器)跑 23 个输入的对抗性语料。

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

On direction: this is a clean correctness and security fix — the OSC parser was accepting malformed sequences where the numeric code wasn't followed by ;, letting hook-controlled input smuggle arbitrary payloads past the allowlist. Clearly within scope, clearly needed. No CHANGELOG reference needed for a parser bug fix.

On approach: minimal and focused — 2 files, +8/-3. The fix is exactly one condition change (input[position] !== ';'), plus a stale comment cleanup, plus 3 new test assertions. No scope creep, no drive-by refactors. This is how bug fixes should look.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:这是一个干净的正确性和安全修复——OSC 解析器此前接受了数字代码后未跟 ; 的非法序列,允许 hook 控制的输入绕过白名单携带任意 payload。明确在范围内,明确需要修复。解析器 bug 修复无需 CHANGELOG 引用。

方案:最小且聚焦——2 个文件,+8/-3。修复就是一个条件的变更(input[position] !== ';'),加上一条过时注释的清理,加上 3 个新测试断言。无范围蔓延,无顺手重构。bug 修复就该这样。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

The fix is a single condition change in parseOscSequence: after reading the numeric code, the parser now requires input[position] === ';' before proceeding to read the payload. Previously it only checked position >= input.length, so any non-terminator byte after the code was silently treated as the start of the payload.

The stale comment OSC 0/1/2 can have just a title with ';' was correctly removed — it no longer matched behavior.

No correctness bugs, no security holes, no regressions. The fix is structurally safe: every valid allowlisted OSC sequence has ; after the code, so no valid input can be affected. Follows project conventions — no over-abstraction, no unrelated changes.

No blockers found.

Testing

Unit tests (PR code)

Suite Result
terminalSequence.test.ts ✅ 26/26 passed (3 new assertions)
Downstream: useTerminalNotification, useAttentionNotifications, notificationService ✅ 24/24 passed

Before/after parser verification

=== BEFORE (main branch - bug present) ===
ESC]9oopsBEL (malformed): ACCEPTED    ← bug: should be rejected
ESC]9BEL (no separator): ACCEPTED     ← bug: should be rejected
ESC]9;hiBEL (valid): ACCEPTED
ESC]0;titleBEL (valid): ACCEPTED

=== AFTER (PR fix applied) ===
ESC]9oopsBEL (malformed): REJECTED    ← fixed ✅
ESC]9BEL (no separator): REJECTED     ← fixed ✅
ESC]9;hiBEL (valid): ACCEPTED         ← no regression
ESC]0;titleBEL (valid): ACCEPTED      ← no regression

Vitest (PR code)

 RUN  v3.2.4 packages/cli
      Coverage enabled with v8

 ✓ src/utils/terminalSequence.test.ts (26 tests) 10ms

 Test Files  1 passed (1)
      Tests  26 passed (26)

2 malformed cases fixed, all valid cases preserved, 0 regressions.

中文说明

代码审查

修复是 parseOscSequence 中的一个条件变更:读取数字代码后,解析器现在要求 input[position] === ';' 才能继续读取 payload。此前仅检查 position >= input.length,代码后的任何非终止符字节都被默默当作 payload 的起始。

过时注释 OSC 0/1/2 can have just a title with ';' 已正确移除——它不再与行为匹配。

无正确性 bug、无安全漏洞、无回归。修复在结构上是安全的:每个合法的白名单 OSC 序列在代码后都有 ;,因此不会影响任何合法输入。遵循项目规范——无过度抽象、无无关改动。

未发现阻塞项。

测试

单元测试(PR 代码)

套件 结果
terminalSequence.test.ts ✅ 26/26 通过(3 个新断言)
下游:useTerminalNotificationuseAttentionNotificationsnotificationService ✅ 24/24 通过

修改前后解析器验证

=== 修改前(main 分支 - bug 存在) ===
ESC]9oopsBEL(非法):被接受    ← bug:应被拒绝
ESC]9BEL(无分隔符):被接受     ← bug:应被拒绝
ESC]9;hiBEL(合法):被接受
ESC]0;titleBEL(合法):被接受

=== 修改后(PR 修复) ===
ESC]9oopsBEL(非法):被拒绝    ← 已修复 ✅
ESC]9BEL(无分隔符):被拒绝     ← 已修复 ✅
ESC]9;hiBEL(合法):被接受     ← 无回归
ESC]0;titleBEL(合法):被接受  ← 无回归

2 个非法用例已修复,所有合法用例保留,0 回归。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This is a textbook bug fix — one condition, one file, one bug. The parser had a gap between its documented contract (ESC ] <code> ; <payload> <terminator>) and its actual behavior (accepting anything after the code as payload). The fix closes that gap with a single character check.

My independent proposal would have been identical: add input[position] !== ';' right after the code-reading loop. The PR matches this exactly, and also correctly removes the stale comment that no longer reflected behavior.

The before/after testing confirms: malformed sequences (ESC]9oopsBEL, ESC]9BEL) go from accepted to rejected, while all valid sequences remain accepted. 26/26 unit tests pass, 24/24 downstream consumer tests pass. Zero regressions by construction — every allowlisted OSC code is followed by ; in valid input.

The security angle is real too: since hooks control terminalSequence input, the old parser let a malicious hook smuggle arbitrary escape payloads past the allowlist. Tightening the boundary closes that path.

Approving. ✅

中文说明

这是一个教科书式的 bug 修复——一个条件、一个文件、一个 bug。解析器的文档化契约(ESC ] <code> ; <payload> <terminator>)与实际行为(接受代码后的任意内容作为 payload)之间存在差距。修复用一个字符检查弥合了这个差距。

我的独立方案完全相同:在代码读取循环之后添加 input[position] !== ';'。PR 与此完全一致,并且正确移除了不再反映行为的过时注释。

修改前后测试确认:非法序列(ESC]9oopsBELESC]9BEL)从被接受变为被拒绝,所有合法序列保持被接受。26/26 单元测试通过,24/24 下游消费方测试通过。结构上零回归——每个白名单 OSC 代码在合法输入中都跟随 ;

安全角度也是真实的:由于 hook 控制 terminalSequence 输入,旧解析器允许恶意 hook 将任意转义 payload 绕过白名单。收紧边界堵住了这条路径。

批准。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao
wenshao merged commit 51d825c into QwenLM:main Jun 18, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(cli): terminalSequence accepts malformed OSC codes

3 participants