From c4a8c98b9dddc9bbcd6220cabe55acb64292fb51 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Tue, 15 Sep 2026 12:58:19 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20AGC=20=E9=97=B9=E9=92=9F*?= =?UTF-8?q?*=E5=8F=AA=E6=9E=9A=E4=B8=BE=E4=B8=80=E6=9D=A1=20ref**=20?= =?UTF-8?q?=E8=80=8C=E5=A3=B0=E7=A7=B0"=E6=95=B4=E4=B8=AA=E8=BF=9C?= =?UTF-8?q?=E7=AB=AF"=20=E2=80=94=E2=80=94=20=E6=94=B9=E6=88=90=E5=AF=B9?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E8=BF=9C=E7=AB=AF=20ref=20=E5=88=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi 2026-09-15 抓到的**第三个洞**,而且他说得对:这是**同一句话换一根轴**。 ## 一、洞:我在"可达性"那侧立了"问不到 ⇒ 红",却在"覆盖面"那侧"看不到 ⇒ 绿" ```js const branch = rb ? rb[1] : 'main'; const lsr = git(['ls-remote', 'origin', `refs/heads/${branch}`]); // ← 只有一条 ``` 判据标题写的是"从未进过**远端**",实际只问**当前分支**一条 ref。于是 **把受污染历史推到旁支、或推一个指向它的 tag** ⇒ 凭证出去了,而这条判据**照样绿**。 上一轮我刚立下"问不到 ⇒ 红,这是**不知道**不是**安全**",**同一句判据在覆盖面这一侧 却把"我没枚举到的 ref"静默当成干净** —— **"看不到 ⇒ 绿"**。 和"把不知道读成安全"是同一句话,只是**宾语换成了覆盖面**而不是可达性。 ## 二、修法(pi 给的,**不需要 fetch 任何对象**) `ls-remote` 问**全部 ref**,对**每一条**的 tip 查该路径是否在其可达历史里。 查明"远端有哪些 ref"是 `ls-remote` 的免费信息,没有理由只问一条。 `git log -- <路径>` 查的正是"**从该 sha 可达的全部历史**",所以未 fetch 的对象也在其列 —— **这一点我实测过才这么写**(不是照着 pi 的话抄)。 ## 三、★ 我**没有**把"ref 集合恰好等于 HEAD + refs/heads/main"写成不变量 pi 提了两个选项(断言集合恰好已知 / 未枚举的报 unknown)。我选第三条: **判"有没有哪条 ref 的可达历史里有那个 blob"** —— 它对**任意** ref 集合都成立。 理由是 `pre-push` 删 ref 那次的教训:**一道闸消费了不属于它管辖的东西**。 把"远端只有一条分支"写成不变量,会在**加第一个 tag / 第一条正常旁支**时误红 —— 那是**为了抓泄露而给日常操作设卡**。要判的性质是"泄露",不是"ref 集合的形态"。 ## 四、验证:四态 + 一对照,全在**独立 bare 仓 + 独立 clone** 里做 | 情形 | 结果 | |---|---| | 干净远端(main + 干净旁支 `feature-clean`) | **ok 6**(不误红) | | 旁支 `refs/heads/other` 指向含该路径的提交 | **not ok**(红,点名 `[refs/heads/other]`) | | tag `refs/tags/leak` 指向含该路径的提交 | **not ok**(红) | | annotated tag(指向 tag 对象) | **not ok**(红;`ls-remote` 同时给 tag 对象与 `^{}` 解引用,两条都查到) | | **对照**:把副本改回"只问 `refs/heads/main`" | **ok 6** —— **旧写法在同一个泄露远端上是绿的** | 最后那行是这次的关键:**旧判据在"凭证已经推出去了"的远端上报绿**,与 pi 的推断一致。 `unresolved` 那一支(`git log ` 解析不了的 ref)也留着了:**报 unknown 红,不当绿** —— **查不了 ≠ 干净**。 ## 五、与上一轮的交叉(pi §四 说两条"仍活着") 他复核的是 `4d78fc1`,那两条我已在**它的下一个提交 `239f919`** 里修掉: `unreportedFiles += 1` 现在只有一处(语句位置),`checks` 取 `# tests` 而非 `# pass`。 现测:`checks=400 pass=399 fail=1 red=4` —— 与他算的 400/399 逐字一致。**内容以 `239f919` 为准。** --- .../electron/test/criteria-hygiene.test.mjs | 80 +++++++++++++------ 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/client/electron/test/criteria-hygiene.test.mjs b/client/electron/test/criteria-hygiene.test.mjs index ea065af..4f40497 100644 --- a/client/electron/test/criteria-hygiene.test.mjs +++ b/client/electron/test/criteria-hygiene.test.mjs @@ -401,42 +401,74 @@ test('★ AGC 真身从未进过远端历史(泄露之后响的闹钟,不是 * 一致 ⇒ 本地历史可信,判它 * 不一致 ⇒ **红**(本地 ref 陈旧或被改过 —— 这种时候"绿"毫无意义) * 问不到 ⇒ **红并明说**"这是不知道,不是安全" + * + * ★★ 而**覆盖面**也要按同一句话判(pi 2026-09-15 抓到的第三个洞): + * 我原来只问 `refs/heads/<当前分支>` **一条 ref**,而这条判据的标题说的是 + * "从未进过**远端**" —— 于是**一次把受污染历史推到旁支、或推一个指向它的 tag**, + * 凭证就出去了,而这条判据**照样绿**。 + * ⇒ 这就是"**把'不知道'读成'安全'**"的**同一句话换一根轴**: + * 可达性那一侧我立了"问不到 ⇒ 红",**覆盖面**这一侧却把 + * "我没枚举到的 ref"**静默当成干净** —— **"看不到 ⇒ 绿"**。 + * + * 修法(pi 给的,**不需要 fetch 任何对象**):`ls-remote` 问**全部 ref**, + * 对**每一条**的 tip 都查该路径是否在其可达历史里。查明"远端有哪些 ref"是 + * `ls-remote` 的免费信息,没有理由只问一条。 + * + * ⚠️ 我**没有**把"远端 ref 集合恰好等于 HEAD + refs/heads/main"写成不变量 —— + * 那会在加第一个 tag / 第一条正常旁支时误红(**为了抓泄露而给日常操作设卡**, + * 与 `pre-push` 删 ref 那次同族:**一道闸消费了不属于它管辖的东西**)。 + * 真正要判的性质是"**有没有哪条 ref 的可达历史里有那个 blob**", + * 它对**任意** ref 集合都成立。 */ - const rb = /refs\/remotes\/origin\/(\S+)/.exec(git(['symbolic-ref', '-q', 'refs/remotes/origin/HEAD']).stdout.trim()); - const branch = rb ? rb[1] : 'main'; - const lsr = git(['ls-remote', 'origin', `refs/heads/${branch}`]); + const lsr = git(['ls-remote', 'origin']); assert.equal(lsr.status, 0, - `问不到远端(\`git ls-remote origin refs/heads/${branch}\` 失败)——\n` + '问不到远端(`git ls-remote origin` 失败)——\n' + ' ★ 这是**不知道**,不是**安全**。一条专门用来抓"绕过"的闹钟,\n' + ' 如果因为"不 fetch / 问不到"就报绿,那它自己就能被绕过。\n' + ' 修法:确认远端可达、`origin` 名字对,再跑这条。'); - const remoteTip = (lsr.stdout.trim().split('\n')[0] || '').split('\t')[0]; - assert.ok(/^[0-9a-f]{40}$/.test(remoteTip), - `远端没有 refs/heads/${branch}(或输出形状不认识)—— 同样按"不知道"处理,不当绿。` + /* + * 解析成 {ref, sha}。跳过 `HEAD`(symbolic,与某条分支同 sha,查它是重复劳动); + * 形状不认识的直接跳过(下面 `remoteRefs.length > 0` 会兜住"全都没认出来")。 + */ + const remoteRefs = []; + for (const line of lsr.stdout.trim().split('\n')) { + const [sha, ref] = line.split('\t'); + if (!sha || !ref || ref === 'HEAD') continue; + if (!/^[0-9a-f]{40}$/.test(sha)) continue; + remoteRefs.push({ ref, sha }); + } + assert.ok(remoteRefs.length > 0, + '远端一条 ref 都没解析出来(或输出形状不认识)—— 按"不知道"处理,不当绿。' + `\n 原始输出:${JSON.stringify(lsr.stdout.slice(0, 200))}`); - const localTip = git(['rev-parse', `refs/remotes/origin/${branch}`]); - assert.equal(localTip.status, 0, - `本地没有 refs/remotes/origin/${branch} —— 无从证明本地历史等于远端,别把"没有"读成"干净"`); - const localSha = localTip.stdout.trim(); - assert.equal(localSha, remoteTip, - '本地 `origin/' + branch + '` 与**远端真值不一致** ——\n' - + ` 本地:${localSha}\n 远端:${remoteTip}\n` - + ' ⇒ 本地那条历史**此刻不可信**,而这条判据要读的正是它。\n' - + ' ★ 这种时候报"绿"是没有意义的(**"ref 被改坏了"与"它其实被推过"是同一个盲区** ——\n' - + ' 我在做变体验证时就亲手把这条 ref 指到过伪造的提交上)。\n' - + ' 修法:`git fetch origin` 把它拉回真值,再跑。'); + /* + * 对**每一条**远端 ref 的 tip 查该路径。`git log -- <路径>` 查的是 + * **从该 sha 可达的全部历史**,所以未 fetch 的对象也在其列(这一点我实测过才这么写)。 + * 解析不了的(annotated tag 指向 tag 对象等)**报 unknown 而不是绿**。 + */ + const published = []; + const unresolved = []; + for (const { ref, sha } of remoteRefs) { + const lg = git(['log', '--oneline', sha, '--', AGC_PATH]); + if (lg.status !== 0) { unresolved.push(`${ref}(${sha.slice(0, 8)})`); continue; } + if (lg.stdout.trim() !== '') { + published.push(`[${ref}] ${sha.slice(0, 8)}\n` + + lg.stdout.trim().split('\n').map(l => ' ' + l).join('\n')); + } + } + assert.deepEqual(unresolved, [], + '这几条远端 ref 的 tip **查不了可达历史**(`git log -- <路径>` 失败)——\n' + + ' ★ 按"不知道"处理,**不当绿**:**查不了 ≠ 干净**。\n' + + ' (常见成因:annotated tag 指向 tag 对象,或该对象本地没有且远端也不可达。)\n' + + ` ${unresolved.join('\n ')}`); - const published = git(['log', `origin/${branch}`, '--oneline', '--', AGC_PATH]); - assert.equal(published.status, 0, - '要能查到 origin/main —— 查不到时不要当成"没有"(那是把"不知道"读成"安全")'); - assert.equal(published.stdout.trim(), '', - 'AGC 客户端凭证的**真身已经进过远端历史** ——\n' + assert.deepEqual(published, [], + `AGC 客户端凭证的**真身已经进过远端历史**(在 ${published.length} 条 ref 上查到)——\n` + ' 它含 `client_secret`/`api_key` 信封与明文 `client_id`/`app_id`,\n' + ' 而本仓镜像是**匿名可 clone 的公开项目**(docs/GITCODE-MIRROR.md)。\n' + ' ★ 处置**不是**"删掉再推"(历史里的 blob 撤不回):按**已泄露**处理 ——\n' + ' 1) 去 AGC 控制台**轮换** client_secret / api_key;\n' + ' 2) 历史清理(filter-repo)+ 强推,并与所有 clone 过的人对齐;\n' + ' 3) 查清是哪次 push 绕过了 `.githooks/pre-push`(`--no-verify`?没装的机器?)。\n' - + ` 涉及提交:\n ${published.stdout.trim().split('\n').join('\n ')}`); + + ` 命中:\n ${published.join('\n ')}`); });