fix(core): reject userinfo URLs in WebFetch validation by VectorPeak · Pull Request #5783 · QwenLM/qwen-code · GitHub
Skip to content

fix(core): reject userinfo URLs in WebFetch validation#5783

Merged
wenshao merged 2 commits into
QwenLM:mainfrom
VectorPeak:codex/webfetch-userinfo-url
Jun 25, 2026
Merged

fix(core): reject userinfo URLs in WebFetch validation#5783
wenshao merged 2 commits into
QwenLM:mainfrom
VectorPeak:codex/webfetch-userinfo-url

Conversation

@VectorPeak

@VectorPeak VectorPeak commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

What this PR does

This PR hardens WebFetch URL validation so http and https URLs with embedded userinfo are rejected before the tool invocation continues. Normal URLs such as https://example.com/page remain valid, while inputs such as https://user:pass@example.com/page, http://user@example.com/page, and encoded userinfo variants are treated as invalid.

Why it's needed

Embedded credentials in URLs are not needed for normal WebFetch usage and can expose sensitive values through validation, confirmation, logging, diagnostic, or error-reporting surfaces. Issue #5782 was accepted as a small, focused credential-security hardening change, and the maintainer bot specifically welcomed rejecting URLs where the parsed username or password component is present, with focused unit coverage.

Reviewer Test Plan

How to verify

Confirm that WebFetch still accepts normal http and https URLs without userinfo.

Confirm that WebFetch rejects URLs containing a username, password, or both before the request proceeds.

Run:

cd packages/core && npx vitest run src/tools/web-fetch.test.ts

cd packages/core && npm run typecheck

Evidence (Before & After)

Before: issue #5782 reports that the existing validation accepts URLs with embedded userinfo as long as they start with http:// or https://.

After: WebFetch rejects userinfo URLs during validation. Local focused test coverage now includes https://user:secret@example.com/page, http://user@example.com/page, https://:secret@example.com/page, and https://%75ser@example.com/page.

Console validation, Windows:

=== Windows validation: WebFetch userinfo URL hardening ===
Scope: focused unit validation and package typecheck; no network fetch is performed.
OS: Microsoft Windows 11 Professional Workstation 10.0.26200
Node: v24.15.0
npm: 11.12.1
Branch: codex/webfetch-userinfo-url
Commit: d464833
Command: cd packages/core; npx vitest run src/tools/web-fetch.test.ts
Test Files: 1 passed
Tests: 22 passed
Focused test exit code: 0
Command: cd packages/core; npm run typecheck
Typecheck exit code: 0

Console validation, WSL:

Distro: Ubuntu 22.04.5 LTS
Kernel: Linux 6.6.114.1-microsoft-standard-WSL2 x86_64 GNU/Linux
Node: v24.15.0
npm: 11.12.1
Branch: codex/webfetch-userinfo-url
Commit: d464833
Command: cd packages/core; npx vitest run src/tools/web-fetch.test.ts
Test Files: 1 passed
Tests: 22 passed
Command: cd packages/core; npm run typecheck
tsc --noEmit passed

Tested on

OS Status
macOS not tested
Windows tested
Linux / WSL tested

Environment (optional)

Windows 11 Professional Workstation 10.0.26200 with Node.js v24.15.0 and npm 11.12.1. WSL validation used Ubuntu 22.04.5 LTS on WSL2 with Linux 6.6.114.1-microsoft-standard-WSL2, Node.js v24.15.0, and npm 11.12.1.

Risk & Scope

  • Main risk or tradeoff: Low. This intentionally rejects a URL form that can carry credentials and is not needed for normal WebFetch usage.
  • Not validated / out of scope: Full npm run preflight and macOS validation have not been run locally.
  • Breaking changes / migration notes: Users who attempted to pass credentials through URL userinfo will need to use a safer supported mechanism instead.
  • Documentation: No docs update appears needed because this is a validation hardening change rather than a new user-facing command, flag, or workflow.

Linked Issues

Fixes #5782

中文说明

What this PR does

该 PR 加强了 WebFetch 的 URL 校验,使包含嵌入式 userinfo 的 httphttps URL 在工具调用继续之前被拒绝。普通 URL(例如 https://example.com/page)仍然有效,而 https://user:pass@example.com/pagehttp://user@example.com/page 以及编码后的 userinfo 变体会被视为无效。

Why it's needed

URL 中嵌入的凭据并不是正常 WebFetch 使用所必需的,并且可能通过校验、确认、日志、诊断或错误报告等表面暴露敏感值。Issue #5782 已被接受为一个小型、聚焦的凭据安全加固改动,维护者机器人也明确欢迎在解析出的用户名或密码组件存在时拒绝该 URL,并补充聚焦的单元测试覆盖。

Reviewer Test Plan

How to verify

确认 WebFetch 仍然接受不包含 userinfo 的普通 httphttps URL。

确认 WebFetch 会在请求继续之前拒绝包含用户名、密码或二者皆有的 URL。

运行:

cd packages/core && npx vitest run src/tools/web-fetch.test.ts

cd packages/core && npm run typecheck

Evidence (Before & After)

Before:issue #5782 报告称,现有校验只要 URL 以 http://https:// 开头,就会接受包含嵌入式 userinfo 的 URL。

After:WebFetch 会在校验阶段拒绝 userinfo URL。本地聚焦测试覆盖了 https://user:secret@example.com/pagehttp://user@example.com/pagehttps://:secret@example.com/pagehttps://%75ser@example.com/page。Windows 与 WSL 控制台验证均已通过。

Tested on

OS Status
macOS not tested
Windows tested
Linux / WSL tested

Environment (optional)

Windows 11 Professional Workstation 10.0.26200,Node.js v24.15.0,npm 11.12.1。WSL 验证使用 Ubuntu 22.04.5 LTS / WSL2,Linux 6.6.114.1-microsoft-standard-WSL2,Node.js v24.15.0,npm 11.12.1。

Risk & Scope

  • Main risk or tradeoff:风险较低。该 PR 有意拒绝一种可携带凭据、且正常 WebFetch 使用不需要的 URL 形式。
  • Not validated / out of scope:尚未在本地运行完整 npm run preflight 和 macOS 验证。
  • Breaking changes / migration notes:曾尝试通过 URL userinfo 传递凭据的用户,需要改用更安全且受支持的机制。
  • Documentation:该改动是校验加固,不是新的用户可见命令、参数或工作流,因此看起来不需要更新文档。

Linked Issues

Fixes #5782

@VectorPeak VectorPeak marked this pull request as ready for review June 23, 2026 17:10
Comment thread packages/core/src/tools/web-fetch.test.ts
@VectorPeak VectorPeak force-pushed the codex/webfetch-userinfo-url branch from e3a9011 to de69257 Compare June 24, 2026 01:54
Comment thread packages/core/src/tools/web-fetch.test.ts Outdated
Comment thread packages/core/src/tools/web-fetch.ts Outdated
Comment thread packages/core/src/tools/web-fetch.ts
VectorPeak and others added 2 commits June 24, 2026 12:33
Add malformed URL cases that pass the initial scheme check but fail URL parsing, covering the validation fallback path.

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
@wenshao

wenshao commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /review

@github-actions

Copy link
Copy Markdown
Contributor
_Qwen Code review request accepted. Review is queued in [workflow run](https://github.com/QwenLM/qwen-code/actions/runs/28075935218)._

@doudouOUC doudouOUC 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.

No issues found. LGTM! ✅

The credential check logic is sound — WHATWG URL parser correctly detects all userinfo forms (user:pass@, user@, :pass@, encoded variants). Existing reviewer suggestions (duplicate test cases, catch-block error message) are valid polish items but not blockers.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

✅ Local build verification (macOS) — merge reference

Verified PR head 10e9ae6df in a clean isolated worktree built from origin/main (85fef8527) with a fresh npm ci. This fills the macOS coverage gap (the PR table marks macOS not tested) and independently confirms @doudouOUC's approval.

Env: macOS 26.5.1 (arm64) · Node v22.22.2 · npm 10.9.7

Results

Check Command Result
Unit tests npx vitest run src/tools/web-fetch.test.ts 24 passed
Typecheck npm run typecheck (tsc --noEmit) ✅ exit 0
Lint eslint web-fetch.ts web-fetch.test.ts ✅ exit 0
Build npm run build ✅ exit 0 (check compiled into dist)
Merge into main git merge-tree origin/main ✅ clean, no conflicts

Mutation test — the new tests are not vacuous

Removing the new try/catch block from the source (keeping the tests) fails exactly 6 tests and nothing else — 2 malformed-URL cases (https://, http://[::1, which exercise the catch) and 4 credential cases (user:secret@, user@, :secret@, %75ser@). Restoring → 24/24 green. This proves the tests actually exercise the new validation path.

Adversarial robustness (WHATWG new URL() on Node 22, independent of the suite)

Input Verdict
https://user@@example.com REJECT (username → user%40)
https://user:pass@@example.com REJECT
https://user%40real@example.com REJECT
HTTPS://User:Pass@example.com REJECT (uppercase scheme)
https://⟨tab⟩user@…, https://user⟨newline⟩@… REJECT (control-char injection)
https://@example.com accept — no actual credentials
https://example.com\@evil.com accept — host is example.com, @evil.com is the path (not an SSRF bypass)
https://foo.com?x=user:pass@bar accept — user:pass@ is in the query string, not userinfo

No credential leak found. The two accepts are correct (they carry no credentials).

Why both the regex and new URL() are needed

new URL() alone would accept http:/example.com, http:example.com, and ftp://example.com (it normalizes/accepts them). The ^https?:// regex is what rejects those; new URL() adds userinfo detection + malformed-authority rejection. Keeping both (with the new explanatory comment) is correct defense-in-depth.

Review threads — all resolved in the current head

  • ✅ Distinct catch message "The 'url' is malformed and could not be parsed." (no longer duplicates the scheme message)
  • ✅ Explanatory comment added above the regex
  • ✅ Malformed cases merged into the existing scheme it.each (no duplicated ftp:///http:… cases)
  • catch branch now has test coverage (https://, http://[::1)

Minor (non-blocking)

The PR description's Evidence block is from a superseded commit (d464833, "22 passed"); the current head 10e9ae6df has 24 tests. Worth refreshing the description before merge — code is correct.

Verdict

LGTM — safe to merge. Surgical, well-tested, no regressions in sibling credentialed-URL subsystems (proxy / git clone / npm registry / redaction paths are separate and unaffected by this validation).

中文版(点击展开)

✅ 本地构建验证(macOS)— 合并参考

在基于 origin/main85fef8527)的独立干净 worktree 中、经全新 npm ci 后验证了 PR head 10e9ae6df。本次验证补齐了 macOS 覆盖空缺(PR 表格中 macOS 标为 not tested),并独立印证了 @doudouOUC 的 approve。

环境: macOS 26.5.1 (arm64) · Node v22.22.2 · npm 10.9.7

结果

检查项 命令 结果
单元测试 npx vitest run src/tools/web-fetch.test.ts 24 通过
类型检查 npm run typechecktsc --noEmit ✅ exit 0
Lint eslint web-fetch.ts web-fetch.test.ts ✅ exit 0
构建 npm run build ✅ exit 0(校验已编译进 dist
合并到 main git merge-tree origin/main ✅ 干净,无冲突

变异测试 — 新增测试并非空过

源码中移除新增的 try/catch 块(保留测试),恰好 6 个测试失败且仅此 6 个 —— 2 个畸形 URL 用例(https://http://[::1,命中 catch 分支)+ 4 个凭据用例(user:secret@user@:secret@%75ser@)。还原后 24/24 全绿。这证明新增测试确实覆盖了新校验逻辑。

对抗性健壮性(Node 22 上的 WHATWG new URL(),独立于测试套件)

输入 判定
https://user@@example.com 拒绝(username → user%40
https://user:pass@@example.com 拒绝
https://user%40real@example.com 拒绝
HTTPS://User:Pass@example.com 拒绝(大写 scheme)
https://⟨tab⟩user@…https://user⟨换行⟩@… 拒绝(控制字符注入)
https://@example.com 放行 —— 没有真实凭据
https://example.com\@evil.com 放行 —— host 是 example.com@evil.com 落在 path(非 SSRF 绕过)
https://foo.com?x=user:pass@bar 放行 —— user:pass@ 在 query 里,非 userinfo

未发现凭据泄漏。两个放行用例是正确的(不含任何凭据)。

为什么 regex 和 new URL() 都需要保留

仅靠 new URL()放行 http:/example.comhttp:example.comftp://example.com(它会归一化/接受它们)。^https?:// 正则负责拒绝这些;new URL() 负责 userinfo 检测 + 畸形 authority 拒绝。两者并存(加上新增注释)是正确的纵深防御。

Review 线程 — 当前 head 已全部解决

  • catch 改为独立报错 "The 'url' is malformed and could not be parsed."(不再与 scheme 报错重复)
  • ✅ 在 regex 上方补充了说明性注释
  • ✅ 畸形 URL 用例合并进既有 scheme it.each(不再重复 ftp:///http:…
  • catch 分支现已有测试覆盖(https://http://[::1

次要(不阻塞)

PR 描述中的 Evidence 取自已被取代的 commit(d464833,"22 passed");当前 head 10e9ae6df 实为 24 个测试。合并前建议刷新描述 —— 代码本身正确。

结论

LGTM —— 可安全合并。 改动精准、测试充分;相邻的携带凭据 URL 子系统(代理 / git clone / npm registry / 脱敏路径)相互独立,不受本校验影响,无回归。

Local build verification by the maintainer; not an automated bot review.

@VectorPeak VectorPeak requested a review from wenshao June 25, 2026 02:03
@wenshao

wenshao commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR @VectorPeak!

Template looks good ✓ — all required sections present and filled in.

On direction: this is a straightforward credential-security hardening. URLs with embedded userinfo (user:pass@host) can leak through logging, error messages, and confirmation prompts. Fixes #5782, squarely in category/security / scope/credential-security. Clear alignment with the project's security posture — no direction concerns.

On approach: the scope is tight — 9 lines of validation logic + 31 lines of tests, 2 files. No unrelated changes, no scope creep. The approach of parsing with new URL() after the regex scheme check is correct defense-in-depth: the regex catches bad schemes (ftp://, http:) that new URL() silently normalizes, while new URL() catches userinfo and malformed authority that the regex misses. Clean and minimal.

Moving on to code review. 🔍

中文说明

感谢贡献 @VectorPeak

模板完整 ✓ — 所有必需章节均已填写。

方向:这是一个直接的凭据安全加固。包含嵌入式 userinfo(user:pass@host)的 URL 可能通过日志、错误消息和确认提示泄漏。修复 #5782,属于 category/security / scope/credential-security。与项目的安全立场完全一致,无方向性问题。

方案:范围紧凑 — 9 行校验逻辑 + 31 行测试,2 个文件。无无关改动,无范围蔓延。在正则 scheme 检查之后用 new URL() 解析是正确的纵深防御:正则捕获 new URL() 会静默归一化的错误 scheme(ftp://http:),而 new URL() 捕获 userinfo 和畸形 authority。简洁且最小化。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: Before reading the diff, my approach was: after the existing ^https?:// regex, add new URL() parsing in a try/catch, check username/password properties, reject with a distinct error. Add tests for user:pass, user-only, password-only, and URL-encoded userinfo.

Diff comparison: The PR's approach matches my proposal exactly — and goes slightly further by also adding malformed-URL test cases (https://, http://[::1) to the catch branch. The implementation is correct and minimal:

  • The regex + new URL() layering is proper defense-in-depth — regex catches bad schemes that new URL() silently normalizes; new URL() catches userinfo and malformed authority.
  • The new comment above the regex (// Regex rejects non-http(s) schemes and malformed authority that new URL() normalizes away.) is a good why comment explaining the dual-check rationale.
  • Error messages are distinct per failure mode — scheme error, credential error, and malformed error are all different strings.
  • No over-abstraction, no drive-by refactors, no scope creep. 2 files, focused change.

No blockers found. ✅

Real-Scenario Testing

Before (installed build — main without PR #5783)

=== BEFORE (main without PR #5783) ===
[PASS] https://example.com/page -> ACCEPT
[PASS] http://example.com -> ACCEPT
[PASS] HTTPS://example.com -> ACCEPT
[FAIL] https://user:secret@example.com/page -> ACCEPT
[FAIL] http://user@example.com/page -> ACCEPT
[FAIL] https://:secret@example.com/page -> ACCEPT
[FAIL] https://%75ser@example.com/page -> ACCEPT
[PASS] ftp://example.com -> REJECT: The 'url' must be a valid URL starting with http:// or https://.
[PASS] http:example.com -> REJECT: The 'url' must be a valid URL starting with http:// or https://.
[FAIL] https:// -> ACCEPT
[FAIL] http://[::1 -> ACCEPT

5/11 passed

The bug is clearly visible: all 4 credential URLs and both malformed URLs are silently accepted.

After (this PR — built from PR head)

=== AFTER (PR #5783 - userinfo URL validation) ===
[PASS] https://example.com/page -> ACCEPT
[PASS] http://example.com -> ACCEPT
[PASS] HTTPS://example.com -> ACCEPT
[PASS] https://user:secret@example.com/page -> REJECT: The 'url' must not include credentials.
[PASS] http://user@example.com/page -> REJECT: The 'url' must not include credentials.
[PASS] https://:secret@example.com/page -> REJECT: The 'url' must not include credentials.
[PASS] https://%75ser@example.com/page -> REJECT: The 'url' must not include credentials.
[PASS] ftp://example.com -> REJECT: The 'url' must be a valid URL starting with http:// or https://.
[PASS] http:example.com -> REJECT: The 'url' must be a valid URL starting with http:// or https://.
[PASS] https:// -> REJECT: The 'url' is malformed and could not be parsed.
[PASS] http://[::1 -> REJECT: The 'url' is malformed and could not be parsed.

11/11 passed

All credential URLs rejected, malformed URLs caught, normal URLs unaffected.

Unit Tests

cd packages/core && npx vitest run src/tools/web-fetch.test.ts
✓ src/tools/web-fetch.test.ts (25 tests) 94ms
Test Files  1 passed (1)
Tests  25 passed (25)
中文说明

代码审查

独立方案: 在阅读 diff 之前,我的方案是:在现有的 ^https?:// 正则之后,用 try/catch 包裹 new URL() 解析,检查 username/password 属性,存在则拒绝并返回独立错误信息。补充 user:pass、仅 user、仅 password、URL 编码 userinfo 的测试用例。

Diff 对比: PR 的方案与我的方案完全一致——并且进一步补充了畸形 URL 测试用例(https://http://[::1)覆盖 catch 分支。实现正确且最小化:

  • 正则 + new URL() 的层级是正确的纵深防御——正则捕获 new URL() 会静默归一化的错误 scheme;new URL() 捕获 userinfo 和畸形 authority。
  • 新增注释解释了双重检查的原因,属于合格的 why 注释。
  • 三种失败模式(scheme 错误、凭据错误、畸形错误)的错误消息各不相同。
  • 无过度抽象、无顺手重构、无范围蔓延。2 个文件,聚焦改动。

未发现阻塞问题 ✅

真实场景测试

改前(main 不含 PR #5783

=== BEFORE (main without PR #5783) ===
[FAIL] https://user:secret@example.com/page -> ACCEPT
[FAIL] http://user@example.com/page -> ACCEPT
[FAIL] https://:secret@example.com/page -> ACCEPT
[FAIL] https://%75ser@example.com/page -> ACCEPT
[FAIL] https:// -> ACCEPT
[FAIL] http://[::1 -> ACCEPT
5/11 passed

Bug 清晰可见:4 个带凭据的 URL 和 2 个畸形 URL 全部被静默放行。

改后(PR head 构建)

=== AFTER (PR #5783 - userinfo URL validation) ===
[PASS] https://user:secret@example.com/page -> REJECT: The 'url' must not include credentials.
[PASS] http://user@example.com/page -> REJECT: The 'url' must not include credentials.
[PASS] https://:secret@example.com/page -> REJECT: The 'url' must not include credentials.
[PASS] https://%75ser@example.com/page -> REJECT: The 'url' must not include credentials.
[PASS] https:// -> REJECT: The 'url' is malformed and could not be parsed.
[PASS] http://[::1 -> REJECT: The 'url' is malformed and could not be parsed.
11/11 passed

所有凭据 URL 被拒绝,畸形 URL 被捕获,正常 URL 不受影响。

单元测试

✓ src/tools/web-fetch.test.ts (25 tests) 94ms
Tests  25 passed (25)

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This is a clean, surgical security fix. The before/after evidence speaks for itself — 6 previously-accepted dangerous inputs (credential URLs, malformed URLs) are now properly rejected, with zero regressions on valid inputs.

Going back to my independent proposal from the code review: the PR's approach matched it exactly and went slightly further with malformed-URL coverage. I don't see a simpler path — 9 lines of validation logic is about as minimal as this gets while still being correct.

The code is straightforward, the why-comment earns its place, error messages are distinct per failure mode, and the test suite is thorough without being bloated. If I were maintaining this in six months, I'd thank the author.

Approving. ✅

中文说明

这是一个干净、精准的安全修复。改前/改后的证据说明了一切——6 个此前被静默接受的危险输入(凭据 URL、畸形 URL)现在被正确拒绝,有效输入零回归。

回顾代码审查中的独立方案:PR 的方案与之完全一致,并进一步补充了畸形 URL 覆盖。我找不到更简路径——9 行校验逻辑在保证正确性的前提下已接近最小化。

代码简洁,why 注释名副其实,三种失败模式的错误消息各不相同,测试充分而不臃肿。如果六个月后维护这段代码,我会感谢作者。

批准 ✅

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 added this pull request to the merge queue Jun 25, 2026
@wenshao wenshao removed this pull request from the merge queue due to the queue being cleared Jun 25, 2026
@VectorPeak

Copy link
Copy Markdown
Contributor Author

@wenshao wenshao added this pull request to the merge queue Jun 25, 2026
Merged via the queue into QwenLM:main with commit da6122c Jun 25, 2026
43 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.

WebFetch should reject URLs containing userinfo

4 participants