Skip to content

fix(desktop): parse NO_PROXY ports strictly - #5498

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/desktop-no-proxy-port-strict
Jun 20, 2026
Merged

fix(desktop): parse NO_PROXY ports strictly#5498
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/desktop-no-proxy-port-strict

Conversation

@tt-a1i

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

Copy link
Copy Markdown
Contributor

What this PR does

Tightens desktop NO_PROXY port parsing so only clean decimal port suffixes become port-scoped bypass rules. Malformed entries like example.com:443abc or [::1]:abc no longer create active proxy bypasses, while valid host:port and bracketed IPv6 port rules still work.

Why it's needed

The previous parser used parseInt, so example.com:443abc became a valid example.com:443 bypass. Bracketed IPv6 had a second failure mode: [::1]:abc fell through as a host-only rule and bypassed every ::1 port.

Reviewer Test Plan

How to verify

Run the focused desktop proxy test and confirm malformed port suffixes do not bypass while valid IPv6 port rules still match.

Commands run locally:

  • bun test apps/electron/src/main/__tests__/network-proxy.test.ts from packages/desktop
  • bunx prettier --check apps/electron/src/main/network-proxy-utils.ts apps/electron/src/main/__tests__/network-proxy.test.ts from packages/desktop
  • bunx eslint --config eslint.config.mjs src/main/network-proxy-utils.ts src/main/__tests__/network-proxy.test.ts from packages/desktop/apps/electron
  • git diff --check

Evidence (Before & After)

Before: example.com:443abc parsed as port 443 and bypassed https://example.com; [::1]:abc degraded into an all-ports ::1 bypass.

After: the same malformed rules stay non-matching, while [::1]:3000 still matches only port 3000.

Tested on

OS Status
macOS tested
Windows not tested
Linux not tested

Environment (optional)

macOS local checkout, Bun desktop workspace tests.

Risk & Scope

  • Main risk or tradeoff: malformed NO_PROXY entries that previously matched accidentally will stop bypassing the proxy.
  • Not validated / out of scope: full desktop app build and full desktop typecheck/lint. Those currently fail on unrelated existing files outside this patch.
  • Breaking changes / migration notes: valid decimal port rules keep the same behavior.

Linked Issues

Fixes #5497

中文说明

这个 PR 做了什么

收紧 desktop NO_PROXY 端口解析,只有干净的十进制端口后缀才会变成带端口限制的绕过规则。example.com:443abc[::1]:abc 这类畸形条目不再创建有效 proxy bypass;合法的 host:port 和方括号 IPv6 端口规则仍然可用。

为什么需要

之前的解析逻辑用了 parseInt,所以 example.com:443abc 会变成合法的 example.com:443 绕过规则。方括号 IPv6 还有第二个失败模式:[::1]:abc 会退化成 host-only 规则,从而绕过所有 ::1 端口。

Reviewer Test Plan

如何验证

运行 focused desktop proxy test,确认畸形端口后缀不会 bypass,同时合法 IPv6 端口规则仍然匹配。

本地执行过的命令:

  • bun test apps/electron/src/main/__tests__/network-proxy.test.ts,目录为 packages/desktop
  • bunx prettier --check apps/electron/src/main/network-proxy-utils.ts apps/electron/src/main/__tests__/network-proxy.test.ts,目录为 packages/desktop
  • bunx eslint --config eslint.config.mjs src/main/network-proxy-utils.ts src/main/__tests__/network-proxy.test.ts,目录为 packages/desktop/apps/electron
  • git diff --check

前后证据

Before:example.com:443abc 被解析成端口 443,会绕过 https://example.com[::1]:abc 会退化成所有端口的 ::1 绕过。

After:同样的畸形规则不会匹配;[::1]:3000 仍然只匹配端口 3000

测试平台

OS 状态
macOS 已测试
Windows 未测试
Linux 未测试

环境

macOS 本地 checkout,Bun desktop workspace tests。

风险和范围

  • 主要风险或取舍:以前因解析宽松而意外匹配的畸形 NO_PROXY 条目,现在不会再绕过代理。
  • 未验证 / 不在范围内:完整 desktop app build 和完整 desktop typecheck/lint;它们目前被本补丁外的既有文件问题阻塞。
  • 破坏性变更 / 迁移说明:合法十进制端口规则保持原行为。

关联 Issue

Fixes #5497

AI Assistance Disclosure

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

@tt-a1i
tt-a1i marked this pull request as ready for review June 20, 2026 16:04
@wenshao

wenshao commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

On direction: This fixes a real correctness bug — parseInt silently truncating "443abc" to 443 means typos in NO_PROXY can create overly broad proxy bypass rules. Security-adjacent issue (proxy bypass), clearly within scope for the desktop platform area. Aligned with issue #5497.

On approach: Scope is tight and minimal — one new parsePort helper, two call-site swaps, and the IPv6 fallthrough fix. The diff also carries Prettier-compliance formatting (arrowParens: "always", line wrapping) which is fine since the root config requires it. No scope creep.

Moving on to code review and testing. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:修复了一个真实的正确性 bug——parseInt 会把 "443abc" 静默截断为 443,导致 NO_PROXY 中的拼写错误可能创建过于宽泛的代理绕过规则。属于安全相关问题,在 desktop 平台范围内。与 issue #5497 对齐。

方案:范围紧凑且最小化——新增一个 parsePort 辅助函数、两处调用替换、以及 IPv6 fallthrough 修复。diff 中的 Prettier 格式化改动(arrowParens: "always"、换行)是合理的,因为根配置要求这些格式。没有范围蔓延。

进入代码审查和测试。🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: add a strict parsePort helper using /^\d+$/ regex + range check, replace the two parseInt call sites, and handle the IPv6 fallthrough so malformed [::1]:abc doesn't silently degrade into a host-only rule that matches all ports.

The PR's approach matches this exactly. parsePort validates digits-only via regex, then checks Number.isInteger(port) && port >= 0 && port <= 65535. The IPv6 branch correctly requires afterBracket === '' for host-only entries and preserves the full bracketed string as the host when the port suffix is malformed — ensuring no accidental match. The regular host:port branch falls through to the same "keep full entry as host" behavior. All correct, no bugs or regressions found.

Formatting changes (arrow parens, line wrapping) are Prettier compliance per the root config (arrowParens defaults to "always"). No scope creep.

Unit tests: 16/16 pass on the PR branch (3 new tests added covering malformed ports and IPv6 port-scoped rules). 13/13 pass on main (pre-fix baseline, without the new test cases).

Before/After Testing

Before (main branch — bug present)

=== NO_PROXY Port Parsing Bug Reproduction ===

Test 1: parseNoProxyRules("example.com:443abc")
  Rules: [{"host":"example.com","port":443,"wildcard":false}]
  shouldBypassProxy("https://example.com/path", rules) => true
  Expected: false (malformed port should NOT create bypass)
  Status: ❌ BUG

Test 2: parseNoProxyRules("[::1]:abc")
  Rules: [{"host":"::1","wildcard":false}]
  shouldBypassProxy("http://[::1]:3000/path", rules) => true
  Expected: false (malformed port should NOT create bypass)
  Status: ❌ BUG

Test 3: parseNoProxyRules("[::1]:3000") (valid — should still work)
  Rules: [{"host":"::1","port":3000,"wildcard":false}]
  shouldBypassProxy("http://[::1]:3000/path", rules) => true
  Expected: true (valid port-scoped rule)
  Status: ✅ CORRECT

Test 4: parseNoProxyRules("example.com:8080") (valid — should still work)
  Rules: [{"host":"example.com","port":8080,"wildcard":false}]
  shouldBypassProxy("http://example.com:8080/path", rules) => true
  Expected: true (valid port-scoped rule)
  Status: ✅ CORRECT

After (PR #5498 — fix applied)

=== NO_PROXY Port Parsing Bug Reproduction ===

Test 1: parseNoProxyRules("example.com:443abc")
  Rules: [{"host":"example.com:443abc","wildcard":false}]
  shouldBypassProxy("https://example.com/path", rules) => false
  Expected: false (malformed port should NOT create bypass)
  Status: ✅ CORRECT

Test 2: parseNoProxyRules("[::1]:abc")
  Rules: [{"host":"[::1]:abc","wildcard":false}]
  shouldBypassProxy("http://[::1]:3000/path", rules) => false
  Expected: false (malformed port should NOT create bypass)
  Status: ✅ CORRECT

Test 3: parseNoProxyRules("[::1]:3000") (valid — should still work)
  Rules: [{"host":"::1","port":3000,"wildcard":false}]
  shouldBypassProxy("http://[::1]:3000/path", rules) => true
  Expected: true (valid port-scoped rule)
  Status: ✅ CORRECT

Test 4: parseNoProxyRules("example.com:8080") (valid — should still work)
  Rules: [{"host":"example.com","port":8080,"wildcard":false}]
  shouldBypassProxy("http://example.com:8080/path", rules) => true
  Expected: true (valid port-scoped rule)
  Status: ✅ CORRECT

Both bugs fixed, no regressions on valid entries.

中文说明

代码审查

独立方案:添加严格的 parsePort 辅助函数(/^\d+$/ 正则 + 范围检查),替换两处 parseInt 调用,并修复 IPv6 fallthrough 使 [::1]:abc 不会静默退化为匹配所有端口的 host-only 规则。

PR 的方案与此完全一致。parsePort 通过正则验证纯数字,然后检查 Number.isInteger(port) && port >= 0 && port <= 65535。IPv6 分支正确要求 afterBracket === '' 才作为 host-only 条目,否则保留完整方括号字符串作为 host——确保不会意外匹配。常规 host:port 分支也有相同的 fallthrough 行为。没有发现 bug 或回归。

格式化改动(箭头函数括号、换行)是根配置 Prettier 合规性(arrowParens 默认为 "always")。没有范围蔓延。

单元测试: PR 分支 16/16 通过(新增 3 个测试覆盖畸形端口和 IPv6 端口规则)。main 基线 13/13 通过(无新测试用例)。

前后对比测试

两个 bug 均已修复,合法条目无回归。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This PR is a textbook minimal bugfix. The problem is real (parseInt silently truncating malformed port strings creates overly broad proxy bypass rules), the fix is tight (strict regex + range check replacing two call sites), and the before/after testing confirms both bugs are gone with no regressions on valid entries.

The parsePort helper is 5 lines and does exactly one thing. The IPv6 fallthrough handling is the subtle part — malformed [::1]:abc no longer degrades into a host-only rule that matches all ports — and the PR gets it right by preserving the full bracketed string as an unmatchable host.

Tests are thorough: 3 new cases covering malformed host:port, malformed IPv6 port, and valid IPv6 port-scoped rules. All 16 pass.

No concerns. Approving.

中文说明

这个 PR 是教科书级的最小 bugfix。问题真实存在(parseInt 静默截断畸形端口字符串会创建过于宽泛的代理绕过规则),修复紧凑(严格正则 + 范围检查替换两处调用),前后对比测试确认两个 bug 均已修复且合法条目无回归。

parsePort 辅助函数 5 行代码,只做一件事。IPv6 fallthrough 处理是微妙的部分——畸形的 [::1]:abc 不再退化为匹配所有端口的 host-only 规则——PR 通过保留完整方括号字符串作为不可匹配的 host 正确处理了这个问题。

测试充分:3 个新用例覆盖畸形 host:port、畸形 IPv6 端口、合法 IPv6 端口规则。全部 16 个测试通过。

没有顾虑。批准。

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 commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification (real build + tests via tmux) — recommend merge

I verified this PR end-to-end on a real checkout (isolated git worktree at PR head 00c8b06e, bun 1.3.14, the real bun:test suite, driven in tmux). All results below run the real parser with real inputs.

Note on CI coverage (worth knowing): the root workspaces list excludes desktop ("!packages/desktop") and this suite imports bun:test, so npm run test:ci --workspaces (the green "Test" jobs) does not run this file. The standard CI gate does not cover network-proxy.test.ts — local bun verification is the primary correctness signal here.

What the fix does

The old parser used parseInt, which silently accepts a leading-digit prefix: parseInt('443abc', 10) === 443. So example.com:443abc became a real example.com:443 bypass, and [::1]:abc (where parseInt returns NaN) fell through to a host-only rule that bypassed every ::1 port. The PR adds a strict parsePort (/^\d+$/ + integer range 0–65535), replaces both parseInt call sites in parseNoProxyRules, and makes the IPv6 branch keep the whole literal as host when the port is malformed. The remainder of the diff is Prettier reformatting.

Why it's safe / inert: a malformed entry is kept as its full string (e.g. host: 'example.com:443abc', host: '[::1]:abc'). Real URL hostnames can never contain : (the URL parser strips the port), so these rules can match neither exactly nor as a .suffix — they become inert and never bypass. Verified against the matching loop.

Results

Check Result
1. PR's own bun:test suite on PR code 16/16 pass
2. A/B — revert only the source to pre-fix, keep PR tests exactly 2 failexample.com:443abc{host:'example.com', port:443} and bypasses https://example.com (Received: true); reproduces #5497
3. Adversarial harness (24 real cases) all pass on PR; 15 fail on pre-fix (8 parseInt-prefix exploits + 4 out-of-range + 3 IPv6 degradations), 9 valid-behavior cases pass on both
4. Strict tsc --noEmit smoke on the (import-free) source clean
5. prettier --check on both files clean
6. ESLint needs desktop workspace deps (not installed in this checkout); CI Lint is green and the edits are Prettier-driven

Decisive evidence

  • The new tests are meaningful (A/B Where is the config saved? #2): with the source reverted to parseInt but the PR's tests kept, exactly the two malformed-port assertions fail — parseNoProxyRules('example.com:443abc') yields {host:'example.com', port:443} and shouldBypassProxy('https://example.com/path', …) returns true. Swapping the source back to parsePort is exactly what turns them green.
  • The fix closes a broad class, not just two examples (如何自定义密钥文件 .env可能与其他文件冲突 #3): the same 24-case harness on both versions shows pre-fix bypass/parse failures for :443abc, :8080x, :80;DROP, :443.0, :443 evil, :0x1bb, :1e3, :+443, out-of-range :65536/:70000/:99999/:999999, and IPv6 [::1]:abc / [::1]:80x / [::1]:3000.5. Meanwhile all 9 valid-behavior cases pass on both versions (valid host:port, leading-zero 08080→8080, boundary 0/65535, valid [::1]:3000, bare [::1] all-ports, wildcard, .suffix, host-only) → no legitimate config is broken.
  • Real-world impact: ProxyDispatcher.dispatch() (network-proxy.ts) routes a request direct, bypassing the proxy, whenever shouldBypassProxy() is true. So before this fix a typo in NO_PROXY (e.g. example.com:443abc) silently routes example.com traffic around a mandated proxy, and [::1]:abc does so for all loopback-IPv6 ports. The fix makes those entries inert so the traffic correctly goes through the proxy.

Minor notes (non-blocking)

  • parsePort accepts 0 (range is 0–65535); parseInt accepted 0 too, so this is unchanged and harmless (0 is not a usable URL port).
  • Out-of-range suffixes (e.g. :99999) now become inert host rules instead of a port>65535 rule; the bypass outcome is identical (never matches), just a cleaner shape.
  • shouldBypassProxy still uses parseInt(parsed.port) for the URL side — correct, since new URL() only ever yields a valid numeric port.

Verdict

Correct, minimal, and security-positive. Fixes #5497, closes a broad class of malformed-NO_PROXY bypasses, and the 24-case differential confirms no regression to any valid rule. LGTM — good to merge.

🇨🇳 中文版(点击展开)

✅ 本地真实构建 + 测试验证(tmux)— 建议合并

我在真实检出环境中端到端验证了本 PR:在 PR head 00c8b06e 上建独立 git worktree,使用 bun 1.3.14 的真实 bun:test 套件,全程在 tmux 中执行。下列结果均针对真实解析器与真实输入。

关于 CI 覆盖(值得注意):workspaces 列表排除了 desktop("!packages/desktop"),且该套件 import 'bun:test',因此 npm run test:ci --workspaces(即那些绿色的 "Test" job)并不会运行此文件。标准 CI 门禁并未覆盖 network-proxy.test.ts,本地 bun 验证才是此 PR 正确性的主要信号。

修复内容: 旧逻辑用 parseInt,会静默接受"数字前缀":parseInt('443abc', 10) === 443。于是 example.com:443abc 变成真实的 example.com:443 绕过;而 [::1]:abcparseInt 返回 NaN)会退化成 host-only 规则,绕过所有 ::1 端口。PR 新增严格的 parsePort/^\d+$/ + 整数范围 0–65535),替换 parseNoProxyRules 中两处 parseInt,并在 IPv6 分支中:端口畸形时把整个字面量保留为 host。diff 其余部分为 Prettier 格式化。

为何安全 / 失效化(inert): 畸形条目被原样保留为完整字符串(如 host: 'example.com:443abc'host: '[::1]:abc')。真实 URL 的 hostname 永远不含 :(URL 解析会剥离端口),因此这些规则既无法精确匹配也无法 .suffix 匹配 —— 它们变成失效规则,永不绕过。已对照匹配循环确认。

结果:

  1. PR 自带 bun:test 套件跑在 PR 代码上:16/16 通过
  2. A/B —— 只把源码还原为修复前、保留 PR 测试:恰好 2 个失败 —— example.com:443abc{host:'example.com', port:443}绕过 https://example.comReceived: true),完整复现 bug(desktop): NO_PROXY accepts malformed port suffixes #5497
  3. 对抗性用例(24 个真实用例):在 PR 代码上全部通过;在修复前有 15 个失败(8 个 parseInt 前缀利用 + 4 个越界端口 + 3 个 IPv6 退化),而 9 个"合法行为"用例在两个版本上都通过
  4. 对(无外部 import 的)源文件做严格 tsc --noEmit 冒烟检查:干净
  5. 两个文件的 prettier --check干净
  6. ESLint:需要 desktop workspace 依赖(本检出未安装);CI Lint 为绿,且改动为 Prettier 驱动。

关键证据:

  • 新测试有效(A/B Where is the config saved? #2): 源码还原成 parseInt、保留 PR 测试时,恰好这两个畸形端口断言失败 —— parseNoProxyRules('example.com:443abc') 得到 {host:'example.com', port:443}shouldBypassProxy('https://example.com/path', …) 返回 true;把源码换回 parsePort 即恰好使其变绿。
  • 修复关闭的是一整类问题,而非仅两个示例(如何自定义密钥文件 .env可能与其他文件冲突 #3): 同一套 24 例在两个版本上运行,修复前在 :443abc:8080x:80;DROP:443.0:443 evil:0x1bb:1e3:+443、越界 :65536/:70000/:99999/:999999、以及 IPv6 [::1]:abc/[::1]:80x/[::1]:3000.5 上均出现解析/绕过错误;与此同时 9 个合法行为用例在两个版本上都通过(合法 host:port、前导零 08080→8080、边界 0/65535、合法 [::1]:3000、裸 [::1] 全端口、通配、.后缀、host-only),即不破坏任何合法配置。
  • 真实影响: ProxyDispatcher.dispatch()network-proxy.ts)在 shouldBypassProxy() 为真时会让请求直连、绕过代理。所以在修复前,NO_PROXY 里的一个笔误(如 example.com:443abc)会让 example.com 流量静默绕过被强制要求的代理[::1]:abc 则会让所有回环 IPv6 端口都绕过。修复后这些条目失效,流量正确地走代理。

补充(非阻塞):

  • parsePort 接受 0(范围 0–65535);parseInt 原本也接受 0,故行为未变且无害(0 不是可用的 URL 端口)。
  • 越界后缀(如 :99999)现在变为失效 host 规则,而非修复前那种 port>65535 规则;绕过结果一致(都不匹配),只是规则形态更干净。
  • shouldBypassProxyURL 侧仍用 parseInt(parsed.port) —— 这是正确的,因为 new URL() 只会给出合法数字端口。

结论: 改动正确、最小、且对安全有正向作用。修复 #5497,关闭了一整类畸形 NO_PROXY 绕过问题,24 例差分确认未对任何合法规则造成回归LGTM,建议合并。

wildcard: boolean;
}

function parsePort(raw: string): number | undefined {

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.

[Suggestion] After /^\d+$/ regex validation, Number(raw) always produces a non-negative integer — Number.isInteger() and port >= 0 are guaranteed true. Only port <= 65535 carries decision value.

Suggested change
function parsePort(raw: string): number | undefined {
return port <= 65535 ? port : undefined;

— DeepSeek/deepseek-v4-pro via Qwen Code /review

const port = parsed.port
? parseInt(parsed.port, 10)
: DEFAULT_PORTS[parsed.protocol];

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.

[Suggestion] The PR converts both parseInt sites in parseNoProxyRules to the strict parsePort(), but shouldBypassProxy still uses bare parseInt(parsed.port, 10). While safe today (URL constructor validates ports at construction time), the inconsistency invites future copy-paste regressions. Consider replacing with parsePort() or adding a comment explaining why parseInt is safe here.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

@wenshao
wenshao merged commit 8368e7f into QwenLM:main Jun 20, 2026
48 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(desktop): NO_PROXY accepts malformed port suffixes

3 participants