Repository navigation
fix(cli): validate GitHub remote hosts - #5327
Conversation
|
Independently verified — the bypass is real and the fix closes it. I reproduced
Legit forms (https / One suggestion (non-blocking): consider
So just: return new URL(remoteUrl).hostname === 'github.com';The current 中文说明我独立验证过了 —— 绕过是真实的,修复把它堵上了。我用
合法形式(https / 一个建议(非阻塞):可以考虑用
所以改成: return new URL(remoteUrl).hostname === 'github.com';当前 |
7e20c09 to
bf6663d
Compare
bf6663d to
cf2d7af
Compare
|
@qwen-code /triage |
✅ Local verification — PR #5327 (
|
| New test | Old behavior | Class |
|---|---|---|
returns false for github.com lookalike hosts |
accepts https://github.com.evil/… |
🔴 security |
returns false when github.com only appears in the path |
accepts https://gitlab.com/owner/github.com-mirror.git |
🔴 security |
returns false for GitHub SSH lookalike hosts |
accepts [email protected]:… |
🔴 security |
getGitHubRepoInfo … SSH URL with an explicit port |
host = github.com:22 ≠ github.com → throws |
🟠 correctness |
The other 27 pass on the old code too → legitimate GitHub remotes are unaffected (no regression). This confirms the tests are real regression guards, not vacuous.
4. Real-git E2E (no execSync mock) — 17/17 pass
I wrote a harness that creates real git repos with real remotes (git init + git remote add) and runs the actual functions against real git remote -v / git remote get-url output:
Remote (real git remote add) |
isGitHubRepository() |
|---|---|
https://github.com/owner/repo.git |
✅ true |
[email protected]:owner/repo.git (SCP) |
✅ true |
ssh://[email protected]/owner/repo.git |
✅ true |
ssh://[email protected]:22/owner/repo.git (port) |
✅ true |
https://GitHub.com/owner/repo.git (uppercase) |
✅ true |
multi-remote: gitlab origin + github upstream |
✅ true |
https://github.com.evil/owner/repo.git |
✅ false |
https://gitlab.com/owner/github.com-mirror.git |
✅ false |
[email protected]:owner/repo.git |
✅ false |
https://[email protected]/owner/repo.git (userinfo phishing) |
✅ false |
https://gitlab.com/owner/repo.git / no remotes |
✅ false |
getGitHubRepoInfo() against real remotes: https / ssh://…:22 / SCP all return {owner, repo}; lookalike throws. The multi-remote case exercises the real multi-line, tab-separated parsing that the mocked unit tests only approximate.
5. Static checks (the PR's own test plan)
| Check | Result |
|---|---|
prettier --check (both files) |
✅ exit 0 |
git diff --check |
✅ exit 0 |
eslint (both files) |
✅ exit 0 |
npm run build --workspace=packages/cli |
✅ exit 0 |
tsc --noEmit (cli) |
✅ exit 0 — zero errors in gitUtils |
| CI | ✅ Lint / CodeQL / Test macOS+Ubuntu+Windows all green |
Note: a fresh checkout initially shows ~200
tscerrors, but they are all in unrelated files (acp-integration/,serve/, …) from stale workspace builds —mainhas them too. Afternpm run buildofcore+acp-bridge,tsc --noEmitfor cli is fully clean. The PR's file is type-clean throughout.
6. Non-blocking suggestions
- Lock in the userinfo-phishing case with a unit test.
https://[email protected]/owner/repo.gitis correctly rejected (new URL(...).hostname === 'evil.com'), but there's no unit test pinning it — it's a classic bypass worth guarding against regressions. - SCP host check is case-sensitive.
isGitHubRemoteUrl(and the pre-existinggetGitHubRepoInfo) usestartsWith('[email protected]:'), so[email protected]:owner/repo.gitis a false-negative. DNS hosts are case-insensitive and the URL path already lowercases vianew URL(); only the SCP branch differs. Low real-world impact (people write it lowercase) and pre-existing, but a.toLowerCase()on the SCP host would make it consistent.
Verdict
LGTM / safe to merge. Correctly fixes the #5326 lookalike-host bypass plus an SSH-port correctness bug, backed by regression tests proven to catch the old behavior and a real-git E2E. No regressions for valid remotes. The §6 items are minor follow-ups, not blockers.
🇨🇳 中文版本(点击展开)
✅ 本地验证 — PR #5327(fix(cli): validate GitHub remote hosts)
作为维护者,我在隔离的 tmux 会话里构建真实测试,在 Linux 上端到端验证了本 PR,其中包含一个真实 git 集成测试(现有单测都 mock 了 execSync,从未真正跑过 git remote -v 的解析)。以下是供合并决策参考的证据。
环境: Linux x86-64 · Node v22.22.2 · Vitest 3.2.4 · 独立 tmux -L pr5327 · 分支 pull/5327/head @ cf2d7af31
结论速览
修复正确且测试充分。它既堵住了 #5326 的「仿冒域名绕过」,又顺带修了一个 SSH 显式端口的正确性 bug;新增的回归测试确实能抓住旧行为(通过对 main 跑这些测试已证明)。真实 git 的端到端测试确认它对真实 git remote -v 输出有效,还额外覆盖了单测未涉及的一个钓鱼向量。合法 remote 无回归。ESLint / typecheck / prettier / build 全部通过,三平台 CI 全绿。LGTM —— 可以合并。 下方有两个不阻塞的小建议。
1. 改动范围与安全背景
- 2 个文件:
gitUtils.ts(+20/−4)、gitUtils.test.ts(+59/−1)。 isGitHubRepository()守护着/setup-github(setupGithubCommand.ts:117),该命令会写入.github/workflows/文件、下载 qwen-code-action 工作流、并打开仓库的 Actions secrets 设置页。旧的守卫只是对整段git remote -v输出做/github\.com/子串匹配 —— 因此仿冒域名、或github.com出现在任意位置(路径等)都会通过。
2. PR 分支上测试通过
npx vitest run src/utils/gitUtils.test.ts
→ Test Files 1 passed (1) | Tests 31 passed (31) (main 为 24)
新增 7 个用例(3 个拒绝类安全用例、3 个接受类 SSH 用例、1 个 SSH 端口的 getGitHubRepoInfo 用例)+ 1 个改写成真实 git remote -v 格式。
3. 漏洞已证明 —— 新测试在 main 旧代码上会失败
我把 PR 的新测试文件对着 main 的旧 gitUtils.ts 跑,恰好 4 个测试失败,每个都对应本 PR 修复的真实 bug:
| 新测试 | 旧行为 | 类别 |
|---|---|---|
returns false for github.com lookalike hosts |
接受 https://github.com.evil/… |
🔴 安全 |
returns false when github.com only appears in the path |
接受 https://gitlab.com/owner/github.com-mirror.git |
🔴 安全 |
returns false for GitHub SSH lookalike hosts |
接受 [email protected]:… |
🔴 安全 |
getGitHubRepoInfo … SSH URL with an explicit port |
host = github.com:22 ≠ github.com → 抛错 |
🟠 正确性 |
其余 27 个在旧代码上也通过 → 合法 GitHub remote 不受影响(无回归)。这证明这些测试是真正的回归守卫,而非空测试。
4. 真实 git 端到端(不 mock execSync)—— 17/17 通过
我写了一个 harness,创建带真实 remote 的真实 git 仓库(git init + git remote add),用真实 git remote -v / git remote get-url 输出跑真实函数:
Remote(真实 git remote add) |
isGitHubRepository() |
|---|---|
https://github.com/owner/repo.git |
✅ true |
[email protected]:owner/repo.git(SCP) |
✅ true |
ssh://[email protected]/owner/repo.git |
✅ true |
ssh://[email protected]:22/owner/repo.git(端口) |
✅ true |
https://GitHub.com/owner/repo.git(大写) |
✅ true |
多 remote:gitlab origin + github upstream |
✅ true |
https://github.com.evil/owner/repo.git |
✅ false |
https://gitlab.com/owner/github.com-mirror.git |
✅ false |
[email protected]:owner/repo.git |
✅ false |
https://[email protected]/owner/repo.git(userinfo 钓鱼) |
✅ false |
https://gitlab.com/owner/repo.git / 无 remote |
✅ false |
getGitHubRepoInfo() 对真实 remote:https / ssh://…:22 / SCP 都返回 {owner, repo};仿冒域名抛错。多 remote 用例还跑通了真实的多行、Tab 分隔解析 —— 这是 mock 单测只能近似的部分。
5. 静态检查(PR 自带的测试计划)
| 检查 | 结果 |
|---|---|
prettier --check(两个文件) |
✅ exit 0 |
git diff --check |
✅ exit 0 |
eslint(两个文件) |
✅ exit 0 |
npm run build --workspace=packages/cli |
✅ exit 0 |
tsc --noEmit(cli) |
✅ exit 0 —— gitUtils 零错误 |
| CI | ✅ Lint / CodeQL / macOS+Ubuntu+Windows 测试全绿 |
注:全新 checkout 一开始会报约 200 个
tsc错误,但全部在无关文件里(acp-integration/、serve/等),属于过期的 workspace 构建产物 ——main上同样存在。构建core+acp-bridge后,cli 的tsc --noEmit完全干净。PR 改动的文件全程类型干净。
6. 不阻塞的建议
- 用单测锁定 userinfo 钓鱼用例。
https://[email protected]/owner/repo.git已被正确拒绝(new URL(...).hostname === 'evil.com'),但没有单测固定它 —— 这是经典绕过手法,值得加测试防回归。 - SCP 主机判断区分大小写。
isGitHubRemoteUrl(以及本就存在的getGitHubRepoInfo)用startsWith('[email protected]:'),所以[email protected]:owner/repo.git会被误判为 false。DNS 主机名大小写不敏感,URL 路径已经通过new URL()转小写,只有 SCP 分支不一致。现实影响很小(大家都写小写)且属既有写法,但对 SCP 主机加个.toLowerCase()会更一致。
结论
LGTM / 可以合并。 正确修复了 #5326 的仿冒域名绕过,外加一个 SSH 端口正确性 bug;有被证明能抓住旧行为的回归测试与真实 git 端到端验证;合法 remote 无回归。第 6 节为小的后续项,不阻塞合并。
Verified locally with real test runs (tmux): full suite, new-tests-against-old-code diff, a real-git integration harness, and the PR's full static-check plan. The harness file was temporary and removed; tracked source is clean at cf2d7af31.
Summary
git remote -vremote URLs instead of searching the whole output forgithub.comgithub.com.evil[email protected]:owner/repo.gitremotesFixes #5326
Test plan
npx vitest run src/utils/gitUtils.test.tsfrompackages/clinpm run typecheck --workspace=packages/clinpm run build --workspace=packages/clinpm run lintnpx prettier --experimental-cli --check packages/cli/src/utils/gitUtils.ts packages/cli/src/utils/gitUtils.test.tsgit diff --checkAI Assistance Disclosure
I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.