Skip to content

fix(cli): validate GitHub remote hosts - #5327

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/github-remote-host-check
Jun 18, 2026
Merged

wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/github-remote-host-check

Conversation

@tt-a1i

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

Copy link
Copy Markdown
Contributor

Summary

  • parse git remote -v remote URLs instead of searching the whole output for github.com
  • reject lookalike hosts such as github.com.evil
  • keep accepting normal GitHub HTTPS and [email protected]:owner/repo.git remotes

Fixes #5326

Test plan

  • npx vitest run src/utils/gitUtils.test.ts from packages/cli
  • npm run typecheck --workspace=packages/cli
  • npm run build --workspace=packages/cli
  • npm run lint
  • npx prettier --experimental-cli --check packages/cli/src/utils/gitUtils.ts packages/cli/src/utils/gitUtils.test.ts
  • git diff --check

AI Assistance Disclosure

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

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Independently verified — the bypass is real and the fix closes it. I reproduced main's /github\.com/ substring test against the new isGitHubRemoteUrl, and all of these flip from accepted (bug) → rejected:

remote URL main this PR
https://github.com.evil/owner/repo.git ✅ accepted ❌ rejected
https://notgithub.com/owner/repo.git ✅ accepted ❌ rejected
https://gitlab.com/me/github.com-mirror.git (substring in path) ✅ accepted ❌ rejected
[email protected]:owner/repo.git ✅ accepted ❌ rejected

Legit forms (https / [email protected]: SCP / ssh:// / :443 / GitHub.com casing) all still pass — nice.

One suggestion (non-blocking): consider new URL(remoteUrl).hostname instead of .host. .host keeps non-default ports, so it wrongly rejects explicit-port GitHub remotes — and .hostname strips the port while still rejecting every lookalike:

remote URL .host (current) .hostname
ssh://[email protected]:22/owner/repo.git ❌ rejected ✅ accepted
https://github.com:8443/owner/repo.git ❌ rejected ✅ accepted
https://github.com.evil/owner/repo.git ❌ rejected ❌ rejected

So just:

return new URL(remoteUrl).hostname === 'github.com';

The current .host behavior is fail-closed (a benign UX miss on an uncommon form), so this is safe to merge as-is — but .hostname is strictly more correct. LGTM either way; flip it out of draft when you're ready. Thanks!

中文说明

我独立验证过了 —— 绕过是真实的,修复把它堵上了。我用 main 的 /github\.com/ 子串判断对照新的 isGitHubRemoteUrl,下面这些全部从被接受(bug)→ 被拒绝:

remote URL main 本 PR
https://github.com.evil/owner/repo.git ✅ 接受 ❌ 拒绝
https://notgithub.com/owner/repo.git ✅ 接受 ❌ 拒绝
https://gitlab.com/me/github.com-mirror.git(子串在路径里) ✅ 接受 ❌ 拒绝
[email protected]:owner/repo.git ✅ 接受 ❌ 拒绝

合法形式(https / [email protected]: SCP / ssh:// / :443 / GitHub.com 大小写)依然全部通过 —— 很好。

一个建议(非阻塞):可以考虑用 new URL(remoteUrl).hostname 而不是 .host。.host 会保留非默认端口,所以会错误拒绝带显式端口的 GitHub remote;而 .hostname 去掉端口、又依然拒绝所有仿冒域名:

remote URL .host(当前) .hostname
ssh://[email protected]:22/owner/repo.git ❌ 拒绝 ✅ 接受
https://github.com:8443/owner/repo.git ❌ 拒绝 ✅ 接受
https://github.com.evil/owner/repo.git ❌ 拒绝 ❌ 拒绝

所以改成:

return new URL(remoteUrl).hostname === 'github.com';

当前 .host 的行为是 fail-closed(只是在不常见形式上有个无害的 UX 小遗漏),所以原样合并也安全 —— 但 .hostname 严格来说更正确。两种都 LGTM;你准备好就翻出 draft。谢谢!

@tt-a1i
tt-a1i force-pushed the fix/github-remote-host-check branch from bf6663d to cf2d7af Compare June 18, 2026 18:13
@tt-a1i
tt-a1i marked this pull request as ready for review June 18, 2026 18:42
@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification — PR #5327 (fix(cli): validate GitHub remote hosts)

As a maintainer, I built real tests in an isolated tmux session and verified this PR end-to-end on Linux, including a real-git integration harness (the existing unit tests mock execSync, so they never exercise actual git remote -v parsing). Evidence below for the merge decision.

Environment: Linux x86-64 · Node v22.22.2 · Vitest 3.2.4 · isolated tmux -L pr5327 · branch pull/5327/head @ cf2d7af31

TL;DR

The fix is correct and well-tested. It closes the #5326 lookalike-host bypass and an SSH-explicit-port correctness bug, with regression tests that genuinely catch the old behavior (proven by running them against main). A real-git E2E confirms it works against actual git remote -v output, including an extra phishing vector the unit tests don't cover. No regressions for legitimate remotes. ESLint / typecheck / prettier / build all clean; CI green on 3 OSes. LGTM — safe to merge. Two small, non-blocking suggestions below.


1. Scope & security context

  • 2 files: gitUtils.ts (+20/−4), gitUtils.test.ts (+59/−1).
  • isGitHubRepository() guards /setup-github (setupGithubCommand.ts:117), which writes .github/workflows/ files, downloads the qwen-code-action workflows, and opens the repo's Actions secrets settings URL. The old guard was a plain substring ‎/github\.com/ test over the whole git remote -v output — so lookalike hosts and github.com appearing anywhere (path, etc.) passed.

2. Tests pass on the PR branch

npx vitest run src/utils/gitUtils.test.ts
→ Test Files 1 passed (1) | Tests 31 passed (31)   (main had 24)

+7 new cases (3 reject security cases, 3 accept SSH cases, 1 SSH-port getGitHubRepoInfo case) + 1 reformatted to realistic git remote -v output.

3. Vulnerability proven — the new tests fail against main's code

I ran the PR's new test file against the OLD gitUtils.ts (checked out from main). Exactly 4 tests fail, each mapping to a real bug this PR fixes:

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 tsc errors, but they are all in unrelated files (acp-integration/, serve/, …) from stale workspace builds — main has them too. After npm run build of core + acp-bridge, tsc --noEmit for cli is fully clean. The PR's file is type-clean throughout.

6. Non-blocking suggestions

  1. Lock in the userinfo-phishing case with a unit test. https://[email protected]/owner/repo.git is correctly rejected (new URL(...).hostname === 'evil.com'), but there's no unit test pinning it — it's a classic bypass worth guarding against regressions.
  2. SCP host check is case-sensitive. isGitHubRemoteUrl (and the pre-existing getGitHubRepoInfo) use startsWith('[email protected]:'), so [email protected]:owner/repo.git is a false-negative. DNS hosts are case-insensitive and the URL path already lowercases via new 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. 不阻塞的建议

  1. 用单测锁定 userinfo 钓鱼用例。 https://[email protected]/owner/repo.git 已被正确拒绝(new URL(...).hostname === 'evil.com'),但没有单测固定它 —— 这是经典绕过手法,值得加测试防回归。
  2. 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.

@wenshao
wenshao merged commit 0f412ac into QwenLM:main Jun 18, 2026
29 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): GitHub remote check accepts github.com lookalike hosts

2 participants