Skip to content

fix(pilot): 选择器的「是不是数字」改成枚举,不受 locale 影响 - #50

Merged
jhfnetboy merged 2 commits into
mainfrom
fix/selector-locale
Aug 5, 2026
Merged

jhfnetboy merged 2 commits into
mainfrom
fix/selector-locale

Conversation

@jhfnetboy

Copy link
Copy Markdown
Member

PR#47 approve 时评审顺手挑出来的一条 Low(Codex 发现的)。它自己说「走 #45,本 PR 不要再加东西」——
但 #45 现在只改 safe-cleanup.sh,把 git-guard.sh 的改动塞进去就是再一次「一个 PR 装多件事」,
所以单独开这个。

问题

[!0-9] 是区间,哪些字符算「在 0 到 9 之间」由 LC_COLLATE 决定。在 fa_IR / ar_SA 下,
东阿拉伯数字 ٥(U+0665) 和 ۵(U+06F5) 落在区间里,于是被当成「纯数字 PR 号」接受。

实测(已合入 main 的那一版):

LC_ALL=fa_IR  git-guard.sh merge-pr "٥" --integration main
  → BLOCKED: integration 'main' is a trunk branch …      ← 已经穿过选择器闸门,死在下一关

修法

枚举,不用区间:*[!0123456789]*。五种 locale 全部拒绝:

LC_ALL=C              ٥ -> BLOCKED: refusing PR selector '٥' …
LC_ALL=en_US.UTF-8    ٥ -> BLOCKED
LC_ALL=fa_IR          ٥ -> BLOCKED
LC_ALL=fa_IR.UTF-8    ٥ -> BLOCKED
LC_ALL=ar_SA.UTF-8    ٥ -> BLOCKED

approvals 那处的同样写法一并改掉。它由 python print() 一个 JSON 整数喂进来、实际只会是 ASCII,
改它是为了让这个文件只有一种判断「是不是数字」的写法,而不是一处防得住 locale、一处防不住。

定界

今天没有可达利用,所以是 Low:那两个字符下一关就死在 gh pr view(cannot read PR #٥ base branch),
而字母 / / / : / . 从来不在被撑大的区间里,所以 URL、分支名、refs/heads/x 那几条拒绝在任何
locale 下都成立 —— 上一轮那条 High 没有回归。但这是一条安全 rail 的第一道闸门,它对「什么是数字」
的判断不该随 locale 变。

回归

数字 47 正常放行(走到 base 检查)、URL 和分支名照拒。preflight.sh run 4/4(grade A)。

https://claude.ai/code/session_01CmAW1q62bBtjT99inZeyLk

PR#47 approve 时评审顺手挑出来的一条 Low(Codex 发现)。

`[!0-9]` 是【区间】,哪些字符算"在 0 到 9 之间"由 LC_COLLATE 决定。
在 fa_IR / ar_SA 下,东阿拉伯数字 ٥(U+0665) 和 ۵(U+06F5) 落在区间内,
于是被当成"纯数字 PR 号"接受。

实测(合入 main 的那版):
  LC_ALL=fa_IR … merge-pr "٥" --integration main
    → 穿过选择器闸门,死在下一关的 trunk 检查

改成枚举 `[!0123456789]` 之后,C / en_US.UTF-8 / fa_IR / fa_IR.UTF-8 /
ar_SA.UTF-8 五种 locale 全部拒绝。

定界(所以只是 Low):今天没有可达利用 —— 那两个字符下一关就死在
`gh pr view` 上,而字母、`/`、`:`、`.` 从来不在被撑大的区间里,所以
URL / 分支名 / refs/heads/x 那几条拒绝在任何 locale 下都成立。但这是
一条安全 rail 的第一道闸门,它对"什么是数字"的判断不该随 locale 变。

approvals 那处的同样写法一并改掉。它由 python print 一个 JSON 整数
喂进来、实际只会是 ASCII,改它是为了让这个文件只有【一种】判断
"是不是数字"的写法,而不是一处防得住 locale、一处防不住。

回归:数字 47 正常放行、URL / 分支名照拒。

Claude-Session: https://claude.ai/code/session_01CmAW1q62bBtjT99inZeyLk
@jhfnetboy
jhfnetboy requested a review from clestons as a code owner August 5, 2026 18:18
jhfnetboy added a commit that referenced this pull request Aug 5, 2026
两处冲突,都是 #47 合入 main 造成的:

- git-guard.sh:整个文件取 main 的版本。白名单已经归 #47(以及 #50 的
  locale 修复)管,这个分支不该再碰它 —— 本 PR 只做 squash 清理。
- followups.md:FU-1 取 main 的 done=PR#47(白名单实际是 #47 合的,
  本分支上标 done=PR#45 是拆分前的旧状态);FU-2/3/4 取本分支的完成
  标记;FU-7 及其撤回、FU-8 保留;FU-9 追加在末尾。十个条目一个没少。

Claude-Session: https://claude.ai/code/session_01CmAW1q62bBtjT99inZeyLk

@clestons clestons left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ APPROVE — [3-round, Codex 按 post-R2 闸门跳过(全 Low)]

这条改对了,而且拆成独立 PR 的判断也对(#45 现在只动 safe-cleanup.sh,把它塞进去就是再一次「一个 PR 装多件事」)。

实测(/bin/bash 3.2.57 arm64-apple-darwin25,merge-pr <sel> --integration zzz-nonexistent):

选择器 locale 结果
47 C / fa_IR.UTF-8 当成数字接受,进到 base 检查 ✅
٥ (U+0665) / ۵ (U+06F5) fa_IR.UTF-8 在选择器闸门被拒 ✅(此前会穿过去)
https://…/pull/26、main fa_IR.UTF-8 仍然被拒 ✅
007、0 C 仍当数字接受,交给 gh 判 ✅
+5、5.0、a/b、空串、含换行 — 仍然被拒 ✅

对 ASCII 输入是严格的 no-op,只在非 ASCII 数字上收紧 —— 这正是想要的形状。第二处(approvals 解析)改动确实是惰性的(python print() 出来的 JSON 整数必是 ASCII),而注释自己老实写了这一点,没有把它包装成一个修复,这点值得表扬。

关于 DeepSeek 提的 [[:digit:]]:那是个更糟的写法,不要采纳

R1 建议改用 POSIX 字符类。实测 6 个 locale × 5 个字符(٥ ۵ ٠ १ 5)共 30 格:

*[!0123456789]*(本 PR) *[![:digit:]]*(R1 的建议)
C 全拒 ✅ 全拒
en_US.UTF-8 全拒 ✅ 全部接受 ⚠️
fa_IR / fa_IR.UTF-8 全拒 ✅ 全部接受
ar_SA.UTF-8 / hi_IN.UTF-8 / ja_JP.UTF-8 全拒 ✅ 全部接受

[[:digit:]] 会把「数字」扩成全部 Unicode Nd,连默认的 en_US.UTF-8 都中招 —— 而旧的区间写法至少还要求 locale 匹配才出问题。所以采纳 R1 会让这个洞在一台默认配置的开发机上就可达。枚举是正确选择。


同一类构造在别处还活着,而且那边是 fail-OPEN

[Low] check-docs.sh:42 —— 同样的 *[!0-9]*,但那里失败方向是反的

在一个七份规划文档各只有一个字符的仓库上实测:

LC_ALL=C            PILOT_DOC_MIN_BYTES=٥   → rc=2  正确拒绝(must be a non-negative integer)
LC_ALL=fa_IR.UTF-8  PILOT_DOC_MIN_BYTES=٥   → rc=0  ok=7/7 "ready — safe to run unattended"
                                              (伴随 [: ٥: integer expression expected × 两处)
LC_ALL=C            (默认 MIN_BYTES)        → rc=1  NOT ready, ok=0/7

set -uo pipefail(没有 -e)让出错的 [ ] 条件读成 false,于是每份文档都掉进 ok 分支 —— 那道 fail-CLOSED 的无人值守起跑门禁 fail-OPEN 了,在一个根本没有规划的仓库上放行。这比本 PR 关掉的那个洞后果更重。

而且它不限于波斯语/阿拉伯语环境:ja_JP.UTF-8 + PILOT_DOC_MIN_BYTES=5(U+FF15) 同样 rc=0 ok=7/7 —— 一个主流开发 locale。

同文件的 :96(case "$bytes" in ''|*[!0-9]*))是同一构造的第二处,今天不可达($bytes 来自 wc -c),但按本 PR 自己的一致性论证(第二处 approvals 那个惰性站点都改了),它也该一起改。

[Low] pr-monitor.sh:61,75,78 —— 同样的构造。PILOT_VERDICT_MAX_MIN=٥ 在受影响 locale 下能穿过校验,随后 deadline=$(( $(date +%s) + max_min * 60 )) 是算术语法错误、deadline 未绑定,set -u 直接中止那个通宵等裁决的循环 —— 正是 :51-57 注释里说这条校验要防的那类死等。

建议把 check-docs.sh 那条放进 #49(那个 PR 本来就在改这个文件),而不是把本 PR 撑大。fail-OPEN 的门禁是更高价值的修复,值得在那边单独写一段复现。

[Low] git-guard.sh:174-180 的新注释 —— 它列的字符和 locale 是当时采样到的,不是受影响集合。我自己的矩阵里,旧的 [!0-9] 还会在 fa_IR/ar_SA 下接受 ٠(U+0660)、在 hi_IN.UTF-8 下接受 १(U+0967)、在 ja_JP.UTF-8 下接受 5(U+FF15)。论证本身不依赖这份清单穷尽,但现在读起来像是「就这两个字符的小毛病」。建议加个「例如」或干脆不列。

顺带确认:注释里「nothing was exploitable」这个结论是成立的,我跨 9 个 locale 复核过 —— 字母、/、:、.、+、-、换行从未进入过那个区间,所以 URL 和分支名的拒绝在每个 locale 下都守住了;被放进去的全都是孤立的非 ASCII 数字,下一关 gh pr view 就死。

建议

  • 三个文件现在需要同一个判据了,考虑抽一个 is_uint() 放进共享文件。四份手抄的 glob 就是下一次漂移的来源 —— 这个仓库前面几轮反复踩的正是这个形状。
  • 独立复核:bash -n 干净;接受/拒绝集合与改动前在 ASCII 上完全一致。

PK Review v4 · 3 轮(R3 Codex 按 post-R2 严重度闸门跳过 —— R2 全部为 Low):R1a DeepSeek-v4-flash 1 条 Low(建议 [[:digit:]],前提为假且修法更糟,30 格 locale 矩阵实测驳回)/ R1b 无发现 → R2 Opus 独立战略评审(自跑 9 locale 矩阵,驳回 R1a 并挖出 check-docs.sh 同构造 fail-OPEN、pr-monitor.sh 三处)→ R4 Opus 全量裁决(把 check-docs 的复现扩到 ja_JP.UTF-8 主流 locale,另补同文件 :96 第二处站点)。机械证据:6 locale × 5 字符共 30 格枚举 vs 字符类对照;merge-pr 在 C/fa_IR 下对数字/东阿拉伯数字/URL/分支名的拒绝矩阵;check-docs.sh 在 C(rc=2) / fa_IR(rc=0 ok=7/7) / ja_JP(rc=0 ok=7/7) 三档的端到端复现;followups.sh 的 POSIX ERE 经实测不受影响(说明本 PR 范围既不窄也不宽)。

@jhfnetboy
jhfnetboy merged commit bf0a0a2 into main Aug 5, 2026
5 checks passed
@jhfnetboy
jhfnetboy deleted the fix/selector-locale branch August 5, 2026 19:48
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 5, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants