Skip to content

feat(core): add --insecure flag to skip TLS verification for self-signed endpoints (#3535) - #5962

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
TianYuan1024:feat/tls-insecure-flag
Jun 29, 2026
Merged

feat(core): add --insecure flag to skip TLS verification for self-signed endpoints (#3535)#5962
wenshao merged 4 commits into
QwenLM:mainfrom
TianYuan1024:feat/tls-insecure-flag

Conversation

@TianYuan1024

Copy link
Copy Markdown
Contributor

What this PR does

Adds an opt-in way to skip TLS certificate verification for outbound model API connections, so Qwen Code can talk to self-hosted endpoints that use a self-signed certificate. It is enabled by a new --insecure CLI flag, the QWEN_TLS_INSECURE environment variable, or the Node convention NODE_TLS_REJECT_UNAUTHORIZED=0. When enabled, verification is disabled on the undici dispatcher that Qwen Code installs: a direct connection uses the connector's TLS options, and a proxied connection disables verification for the upstream origin (and for an HTTPS proxy whose own certificate is self-signed). When not enabled, behavior is unchanged.

Why it's needed

Because Qwen Code installs its own undici dispatcher (to control request timeouts for local backends), Node's global NODE_TLS_REJECT_UNAUTHORIZED=0 was not reliably honored for model API calls, so users pointing Qwen Code at a self-hosted server with a self-signed certificate hit Connection error. (cause: fetch failed) with no escape hatch — even though the same setting works elsewhere. This gives a clear, intentional opt-out for trusted lab / self-hosted environments, while keeping the safer NODE_EXTRA_CA_CERTS (trust the CA) as the recommended path. The flag carries an explicit man-in-the-middle warning in both --help and the troubleshooting docs.

Reviewer Test Plan

How to verify

  1. Stand up (or point at) an HTTPS endpoint with a self-signed certificate, e.g. an OpenAI-compatible local server at https://localhost:8443.
  2. Baseline: qwen --auth-type openai --openaiBaseUrl https://localhost:8443 --openaiApiKey sk-x --prompt "2+2?" → expect Connection error. (cause: fetch failed) and a troubleshooting hint mentioning NODE_EXTRA_CA_CERTS and --insecure.
  3. With the flag: add --insecure to the same command → expect the request to reach the server (no TLS rejection). QWEN_TLS_INSECURE=1 qwen ... and NODE_TLS_REJECT_UNAUTHORIZED=0 qwen ... should behave the same.
  4. Confirm the default is unchanged: without any of the three, verification still rejects the self-signed cert.

Unit coverage: packages/core/src/utils/runtimeFetchOptions.test.ts asserts the direct Agent uses connect: { rejectUnauthorized: false }, the ProxyAgent uses requestTls/proxyTls (not connect), falsy QWEN_TLS_INSECURE values are ignored, and secure/insecure dispatchers are cached separately. packages/cli/src/config/config.test.ts covers flag parsing.

Evidence (Before & After)

N/A (non-TUI; network/TLS behavior). See test output below.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Local unit tests (vitest) on macOS; repo-wide lint / build / typecheck green.

Risk & Scope

  • Main risk or tradeoff: disabling verification removes MITM protection; mitigated by being strictly opt-in, off by default, and warned about in --help and docs.
  • Not validated / out of scope: MCP server transport (EnvHttpProxyAgent) and telemetry use independent paths and are intentionally left verifying; this PR scopes to model API connections, which is what the issue asks for.
  • Breaking changes / migration notes: none — default behavior is unchanged.

Linked Issues

Fixes #3535

中文说明

这个 PR 做了什么

新增一个可选项,用于在访问模型 API 时跳过 TLS 证书校验,使 Qwen Code 能连接使用自签名证书的自建服务。可通过新增的 --insecure 命令行参数、QWEN_TLS_INSECURE 环境变量,或 Node 习惯的 NODE_TLS_REJECT_UNAUTHORIZED=0 任一开启。开启后,会在 Qwen Code 自建的 undici dispatcher 上关闭校验:直连走连接器的 TLS 选项;走代理时关闭对上游源站的校验(以及自签名 HTTPS 代理自身证书的校验)。未开启时行为完全不变。

为什么需要

由于 Qwen Code 为控制本地后端的请求超时而自建了 undici dispatcher,Node 全局的 NODE_TLS_REJECT_UNAUTHORIZED=0 对模型 API 调用并不可靠生效,导致用户指向自签名证书的自建服务器时遇到 Connection error. (cause: fetch failed) 且没有任何逃生通道——尽管同样的设置在别处有效。本改动提供一个明确、需主动开启的开关,面向受信任的实验/自建环境,同时仍把更安全的 NODE_EXTRA_CA_CERTS(信任 CA)作为推荐方案。该参数在 --help 和排障文档中都带有中间人攻击风险提示。

审阅测试计划

如何验证

  1. 准备一个使用自签名证书的 HTTPS 端点(例如本地 OpenAI 兼容服务 https://localhost:8443)。
  2. 基线:qwen --auth-type openai --openaiBaseUrl https://localhost:8443 --openaiApiKey sk-x --prompt "2+2?" → 预期报 Connection error. (cause: fetch failed),并提示 NODE_EXTRA_CA_CERTS--insecure
  3. 加上参数:同样命令加 --insecure → 预期请求能到达服务器(不再被 TLS 拒绝)。QWEN_TLS_INSECURE=1 qwen ...NODE_TLS_REJECT_UNAUTHORIZED=0 qwen ... 行为相同。
  4. 确认默认不变:三者都不设置时,自签名证书仍被拒绝。

单测覆盖:runtimeFetchOptions.test.ts 断言直连 Agent 使用 connect: { rejectUnauthorized: false }、ProxyAgent 使用 requestTls/proxyTls(而非 connect)、忽略 QWEN_TLS_INSECURE 的假值、以及 secure/insecure dispatcher 分开缓存;config.test.ts 覆盖参数解析。

证据(前后对比)

N/A(非 TUI;属网络/TLS 行为),见测试输出。

测试平台

仅在 macOS 本地跑了单测;全仓 lint / build / typecheck 通过。

风险与范围

  • 主要风险:关闭校验会失去中间人防护;通过严格可选、默认关闭、并在 --help 与文档中警告来缓解。
  • 未验证/超出范围:MCP 传输(EnvHttpProxyAgent)与遥测走独立路径,本 PR 有意保持校验;范围限定为模型 API 连接,即 issue 的诉求。
  • 破坏性变更:无,默认行为不变。

关联 Issue

Fixes #3535

…ned endpoints (QwenLM#3535)

Enable skipping TLS certificate verification for outbound model API connections via a new --insecure CLI flag, QWEN_TLS_INSECURE, or NODE_TLS_REJECT_UNAUTHORIZED=0. The setting is applied to the undici dispatcher Qwen Code installs: a direct connection uses connect TLS options, while a proxied connection disables verification for the upstream origin (requestTls) and a self-signed HTTPS proxy (proxyTls). Off by default; behavior is unchanged when not enabled.

Fixes QwenLM#3535

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread packages/cli/src/config/config.ts
Comment thread packages/core/src/utils/runtimeFetchOptions.ts Outdated
Comment thread packages/cli/src/config/config.test.ts
Comment thread packages/core/src/utils/fetch.ts Outdated
…wenLM#3535)

- Block project .env from enabling QWEN_TLS_INSECURE by adding it to PROJECT_ENV_HARDCODED_EXCLUSIONS, so an untrusted repo cannot silently disable TLS verification for all API connections.
- Remove the unverifiable Bun fetch `tls` special-casing; Bun users can still opt out via NODE_TLS_REJECT_UNAUTHORIZED=0, which Bun honors natively.
- Do not suggest `--insecure` in the TLS error hint when verification is already disabled; show a network/protocol-oriented message instead.
- Add tests: env-flag regex/falsy branches, loadCliConfig env side-effect, fetch hint variants, and a security guard for the project .env exclusion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@TianYuan1024

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Addressed all four points in 1417d458c:

  1. [Critical] QWEN_TLS_INSECURE could be set by a project .env — Added QWEN_TLS_INSECURE to PROJECT_ENV_HARDCODED_EXCLUSIONS in shared-env-keys.ts. Since RELOAD_EXCLUDED_KEYS spreads that list, this covers both the initial load and reload paths, so an untrusted project .env can no longer disable TLS verification. Opt-in remains available via the --insecure flag, the shell environment, or a home-scoped .env. Added a regression guard test.

  2. [Critical] Bun tls option is a no-op — Removed the Bun-specific tls handling entirely rather than ship something I couldn't verify on a Bun runtime. Bun users can still opt out via NODE_TLS_REJECT_UNAUTHORIZED=0, which Bun honors natively (and which isTlsVerificationDisabled() already recognizes). The incorrect comment is gone.

  3. [Suggestion] Test gaps — Added tests for the isTlsVerificationDisabled() regex/falsy branches (true/TRUE/yes/on/" 1 " and false/no/off/""), the loadCliConfig env side-effect (flag → QWEN_TLS_INSECURE), and both formatFetchErrorForUser hint branches. (The Bun-runtime test is now moot since the Bun path was removed.)

  4. [Suggestion] Hint shown even when already insecureformatFetchErrorForUser now branches on isTlsVerificationDisabled(): when verification is already disabled it points to a likely network/protocol cause instead of re-suggesting --insecure.

Comment thread packages/cli/src/config/shared-env-keys.ts Outdated
Comment thread packages/core/src/utils/runtimeFetchOptions.ts
Comment thread packages/core/src/utils/runtimeFetchOptions.ts
Comment thread packages/cli/src/config/config.ts
Comment thread packages/core/src/utils/runtimeFetchOptions.ts Outdated
- Block NODE_TLS_REJECT_UNAUTHORIZED from project .env too (initial load only consults PROJECT_ENV_HARDCODED_EXCLUSIONS), since isTlsVerificationDisabled() honors it.
- When opting out, set NODE_TLS_REJECT_UNAUTHORIZED=0 process-wide in loadCliConfig and emit a stderr MITM warning. This makes the opt-out effective on the Bun runtime and the proxy-creation fallback path (which the undici dispatcher does not cover), and gives a user-visible signal.
- Avoid evaluating isTlsVerificationDisabled() twice on the proxy path via a default parameter on getOrCreateSharedDispatcher.
- Update tests accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@TianYuan1024
TianYuan1024 force-pushed the feat/tls-insecure-flag branch from eaac310 to 59efc10 Compare June 29, 2026 03:39
Comment thread packages/cli/src/config/config.ts Outdated
Comment thread packages/cli/src/config/config.ts Outdated
Comment thread packages/cli/src/config/config.test.ts
…wenLM#3535)

- Broaden the TLS-disabled warning to state the process-wide blast radius (API, OAuth, MCP servers, child processes), since NODE_TLS_REJECT_UNAUTHORIZED=0 is set process-wide.
- Also emit the warning via debugLogger so the state is discoverable in ~/.qwen/debug/ after terminal scrollback is gone.
- Add tests for the env-var-only path (pre-set QWEN_TLS_INSECURE) and the already-disabled guard (no duplicate assignment/warning).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@TianYuan1024

Copy link
Copy Markdown
Contributor Author

Thanks @wenshao for the thorough review — addressed everything across 59efc10ed and 755a9c723.

First batch (59efc10ed):

  1. [Critical] NODE_TLS_REJECT_UNAUTHORIZED missing from PROJECT_ENV_HARDCODED_EXCLUSIONS — Added it alongside QWEN_TLS_INSECURE, so a project .env can no longer bypass TLS via either key at initial load (RELOAD_EXCLUDED_KEYS already covered the reload path). Added a guard test.

  2. [Critical] --insecure no-op on Bun / misleading messageloadCliConfig now sets NODE_TLS_REJECT_UNAUTHORIZED=0 process-wide whenever the opt-out is active. Bun honors that natively, so --insecure is now effective there and the "already disabled" hint is accurate. Skipped when the user already set it, to avoid double-warning over Node's own.

  3. [Suggestion] Proxy-failure fallback drops --insecure — The same process-wide setting covers the fallback path (the built-in fetch honors NODE_TLS_REJECT_UNAUTHORIZED).

  4. [Suggestion] isTlsVerificationDisabled() evaluated twice on the proxy pathgetOrCreateSharedDispatcher now receives the resolved insecure via a default parameter; the proxy path passes the already-computed value.

  5. [Suggestion] No user-visible warningloadCliConfig now warns on stderr when verification is disabled.

Second batch (755a9c723):

  1. [Suggestion] Warning understates the blast radius — Broadened the message to state that all HTTPS connections in the process (API calls, OAuth, MCP servers, child processes) are affected, since the setting is process-wide.

  2. [Suggestion] No debugLogger record — The warning now also goes through debugLogger.warn, so the state is discoverable in ~/.qwen/debug/ after the terminal scrollback is gone.

  3. [Suggestion] Missing test paths — Added tests for the env-var-only path (pre-set QWEN_TLS_INSECURE without --insecure) and the already-disabled guard (NODE_TLS_REJECT_UNAUTHORIZED=0 preset → no re-assignment, no duplicate warning).

Design note: the undici dispatcher options are kept as the deterministic guarantee for the Node path (not reliant on runtime env propagation into undici); the process-wide env var covers the runtimes/paths the dispatcher doesn't (Bun, proxy fallback, and non-dispatcher HTTPS).

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

All R1 findings are addressed in this round. The warning now correctly covers the full blast radius (API calls, OAuth, MCP servers, child processes), debugLogger.warn is emitted alongside the console warning, and the test gaps (env-var-only path, NODE_TLS already-'0' guard, regex branches) are filled.

Build passes, all 312 tests pass, deterministic analysis (tsc + eslint) is clean.

LGTM ✅

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer local verification — real self-signed TLS + real qwen binary E2E

Tested at: commit 755a9c7 (== current PR head) · macOS (darwin) · Node v22.22.2 · isolated git worktree off origin/main.

For a security feature I exercised the real code paths against a real self-signed HTTPS endpoint (OpenSSL cert, subject == issuer == CN=localhost): unit suites, a dispatcher-level harness driving the compiled buildRuntimeFetchOptions() with real undici, mutation testing, and a full end-to-end run of the actual qwen binary. The feature works across all three triggers and is secure by default. One scope-accuracy discrepancy in the PR description is noted in §6 for you to reconcile before merge.

1. Build & typecheck — clean

tsc --build @qwen-code/qwen-code-core → exit 0 · tsc --noEmit packages/cli → exit 0.

2. Unit tests — 4 files, 312 tests, all pass

vitest run runtimeFetchOptions.test.ts fetch.test.ts config.test.ts shared-env-keys.test.ts312 passed. Notably config.test.ts (241) asserts --insecureQWEN_TLS_INSECURE=1 and NODE_TLS_REJECT_UNAUTHORIZED=0, and fetch.test.ts asserts the troubleshooting hint contains both NODE_EXTRA_CA_CERTS and --insecure (and omits them once verification is already disabled).

3. Real self-signed TLS — dispatcher harness (35/35)

Harness imports the compiled runtimeFetchOptions.js, builds the real dispatcher, and hits a real self-signed https://localhost server with real undici fetch. The unit tests only assert the options shape against a mocked undici; this proves those options actually change socket behavior:

env at request time real outcome
(none — default) REJECTEDcause = DEPTH_ZERO_SELF_SIGNED_CERT
QWEN_TLS_INSECURE=1 / yes 200 OK (real body returned)
QWEN_TLS_INSECURE=0 / enabled (falsy/unrecognized) REJECTED
NODE_TLS_REJECT_UNAUTHORIZED=0 200 OK
NODE_TLS_REJECT_UNAUTHORIZED=1 REJECTED

Plus the full isTlsVerificationDisabled() parsing matrix (24 cases: 1/true/yes/on incl. case-insensitive + trimmed → enabled; 0/false/no/off/""/2/enabled → ignored) and dispatcher cache separation (secure ≠ insecure instance; same args → same cached instance) — all pass.

4. Real qwen binary — end-to-end (packages/cli/dist/index.js)

Ran the actual CLI against a self-signed HTTPS OpenAI-compatible server that logs every request whose TLS handshake succeeded:

run result requests reaching server
baseline (no flag) API Error: Connection error. (cause: fetch failed) 0 (TLS rejected)
--insecure MITM warning printed + model reply PONG_FROM_SELF_SIGNED_SERVER returned 2 (TLS bypassed)
QWEN_TLS_INSECURE=1 (env) model reply returned reached server
NODE_TLS_REJECT_UNAUTHORIZED=0 (env) model reply returned reached server

The --insecure run surfaces both the project's own warning "TLS certificate verification is disabled … vulnerable to man-in-the-middle attacks" and Node's native NODE_TLS_REJECT_UNAUTHORIZED=0 warning. Secure-by-default is confirmed: the baseline run is rejected and never reaches the server.

5. Mutation testing — the guard is load-bearing & tests are not vacuous

Removed connect: { rejectUnauthorized: false } from the no-proxy Agent, rebuilt, and re-ran:

  • unit: 2 reddisables verification on the no-proxy Agent via QWEN_TLS_INSECURE, honors NODE_TLS_REJECT_UNAUTHORIZED=0 for parity.
  • real harness: QWEN_TLS_INSECURE=1 flips from 200 → REJECTED (proves the connect option, not something incidental, is what enables the bypass for that path).
  • (interesting) NODE_TLS_REJECT_UNAUTHORIZED=0 still connected after the mutation — i.e. a plain undici Agent independently honors the global env here. Restored + rebuilt; harness back to 35/35.

6. ⚠️ Scope-accuracy discrepancy (please reconcile before merge)

The PR's Risk & Scope says: "MCP server transport (EnvHttpProxyAgent) and telemetry use independent paths and are intentionally left verifying; this PR scopes to model API connections." The actual behavior is broader:

  • config.ts sets NODE_TLS_REJECT_UNAUTHORIZED='0' process-wide whenever insecure is enabled (verified — and asserted by config.test.ts).
  • grep rejectUnauthorized packages/core/src (outside runtimeFetchOptions.ts) returns nothing: MCP transport, telemetry, OAuth and the IDE client do not pin verification, so they inherit that global. §5 empirically showed a plain undici Agent honors NODE_TLS_REJECT_UNAUTHORIZED=0.

So --insecure disables TLS verification process-wide, not just for model API calls — exactly what the runtime warning correctly states ("All HTTPS connections in this process (API calls, OAuth, MCP servers, child processes)…"). The warning is honest; the "intentionally left verifying" bullet in the description contradicts it. Not a code bug — the broad, clearly-warned opt-out is a defensible design — but the description should be corrected so a reviewer/operator isn't misled into thinking MCP/telemetry stay verified.

Verdict

Functionally LGTM, with one doc fix requested. Build clean, 312 unit tests green, 35/35 real-TLS checks green, real-binary E2E flips reject → connect under --insecure with a clear MITM warning, secure-by-default confirmed, mutation proves the guard is real. Suggest reconciling the §6 scope wording (the implementation is process-wide; the warning already says so). CI: Test (ubuntu-latest) — the required static-check leg — is green; mac/win show skipping (named-job placeholders), consistent with this repo's required-check mechanism.

🇨🇳 中文版(完整对应)

✅ 维护者本地验证 —— 真实自签名 TLS + 真实 qwen 二进制 E2E

测试基准: commit 755a9c7(== 当前 PR head)· macOS (darwin) · Node v22.22.2 · 基于 origin/main 的独立 git worktree。

作为安全特性,我针对一个真实的自签名 HTTPS 端点(OpenSSL 证书,subject == issuer == CN=localhost)跑通了真实代码路径:单测套件、用编译产物 buildRuntimeFetchOptions() + 真实 undici 驱动的 dispatcher 级 harness、变异测试,以及真实 qwen 二进制的端到端运行。该特性在三种触发方式下都生效,且默认安全。PR 描述里有一处范围表述不准,见 §6,建议合并前修正。

1. 构建与类型检查 —— 干净

tsc --build @qwen-code/qwen-code-core → exit 0 · tsc --noEmit packages/cli → exit 0。

2. 单元测试 —— 4 文件,312 测试,全部通过

vitest run runtimeFetchOptions.test.ts fetch.test.ts config.test.ts shared-env-keys.test.ts312 passed。其中 config.test.ts(241)断言 --insecureQWEN_TLS_INSECURE=1 NODE_TLS_REJECT_UNAUTHORIZED=0fetch.test.ts 断言排障提示同时包含 NODE_EXTRA_CA_CERTS--insecure(且在已禁用校验时省略它们)。

3. 真实自签名 TLS —— dispatcher harness(35/35

harness 导入编译后runtimeFetchOptions.js,构建真实 dispatcher,用真实 undici fetch 访问真实的自签名 https://localhost 服务。单测只针对被 mock 的 undici 断言选项形状;本步证明这些选项真的改变了 socket 行为:

请求时的 env 真实结果
(无 —— 默认) 被拒 —— cause = DEPTH_ZERO_SELF_SIGNED_CERT
QWEN_TLS_INSECURE=1 / yes 200 OK(返回真实 body)
QWEN_TLS_INSECURE=0 / enabled(假值/不识别) 被拒
NODE_TLS_REJECT_UNAUTHORIZED=0 200 OK
NODE_TLS_REJECT_UNAUTHORIZED=1 被拒

外加完整的 isTlsVerificationDisabled() 解析矩阵(24 例:1/true/yes/on 含大小写不敏感 + 去空白 → 启用;0/false/no/off/""/2/enabled → 忽略),以及 dispatcher 缓存分离(secure ≠ insecure 实例;相同参数 → 同一缓存实例)—— 全部通过。

4. 真实 qwen 二进制 —— 端到端(packages/cli/dist/index.js

用真实 CLI 访问一个会记录「TLS 握手成功后到达的每个请求」的自签名 HTTPS OpenAI 兼容服务:

运行 结果 到达服务器的请求数
基线(无 flag) API Error: Connection error. (cause: fetch failed) 0(TLS 被拒)
--insecure 打印 MITM 警告 + 返回模型回复 PONG_FROM_SELF_SIGNED_SERVER 2(TLS 被绕过)
QWEN_TLS_INSECURE=1(env) 返回模型回复 到达服务器
NODE_TLS_REJECT_UNAUTHORIZED=0(env) 返回模型回复 到达服务器

--insecure 运行同时打印了项目自身的警告*「TLS certificate verification is disabled … vulnerable to man-in-the-middle attacks」*以及 Node 原生的 NODE_TLS_REJECT_UNAUTHORIZED=0 警告。默认安全已确认:基线运行被拒、从未到达服务器。

5. 变异测试 —— 守卫是承重的,测试非空过

从 no-proxy Agent 去掉 connect: { rejectUnauthorized: false },重建并复跑:

  • 单测:2 个红 —— disables verification on the no-proxy Agent via QWEN_TLS_INSECUREhonors NODE_TLS_REJECT_UNAUTHORIZED=0 for parity
  • 真实 harness:QWEN_TLS_INSECURE=1200 → 被拒(证明是 connect 选项、而非别的偶然因素,使该路径绕过校验)。
  • (有意思)变异后 NODE_TLS_REJECT_UNAUTHORIZED=0 仍能连上 —— 即此处普通 undici Agent 会独立遵从该全局 env。已还原 + 重建;harness 回到 35/35。

6. ⚠️ 范围表述不准(建议合并前修正)

PR 的 Risk & Scope 写道:「MCP server transport(EnvHttpProxyAgent)与 telemetry 走独立路径,有意保持校验;本 PR 范围限定为模型 API 连接。」 实际行为更广:

  • 只要启用 insecure,config.ts进程级设置 NODE_TLS_REJECT_UNAUTHORIZED='0'(已验证,且 config.test.ts 有断言)。
  • grep rejectUnauthorized packages/core/src(除 runtimeFetchOptions.ts 外)无任何命中:MCP 传输、telemetry、OAuth、IDE client 都没有独立钉住校验,因此会继承该全局;§5 已实证普通 undici Agent 遵从 NODE_TLS_REJECT_UNAUTHORIZED=0

所以 --insecure进程级关闭 TLS 校验,而不仅是模型 API —— 这正是运行时警告所如实描述的(「All HTTPS connections in this process(API calls, OAuth, MCP servers, child processes)…」)。警告是诚实的;描述里*「有意保持校验」*那条与之矛盾。这不是代码 bug —— 范围广但有明确警告的 opt-out 是合理设计 —— 但建议修正描述,以免评审者/运维误以为 MCP/telemetry 仍在校验。

结论

功能上 LGTM,仅建议修一处文档。 构建干净、312 单测全绿、35/35 真实 TLS 检查全绿、真实二进制 E2E 在 --insecure 下把「拒绝」翻转为「连接」并带清晰 MITM 警告、默认安全已确认、变异证明守卫真实有效。建议把 §6 的范围措辞对齐(实现是进程级,警告也已如此说明)。CI:Test (ubuntu-latest)(必需的静态检查腿)为绿;mac/win 显示 skipping(named-job 占位),与本仓库 required-check 机制一致。

@wenshao

wenshao commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @TianYuan1024!

Template looks good ✓ — all required sections present, bilingual, test plan included.

On direction: This solves a real pain point. Users pointing Qwen Code at self-hosted endpoints with self-signed certs have no escape hatch because the custom undici dispatcher (installed for timeout control) doesn't reliably honor NODE_TLS_REJECT_UNAUTHORIZED=0. The --insecure flag is standard across CLI tools (curl -k, wget --no-check-certificate) and the opt-in model with explicit MITM warnings is the right security posture. Claude Code CHANGELOG has no direct reference for this feature, but the problem space (custom TLS trust for enterprise/lab environments) is well-aligned with qwen-code's audience.

On approach: The scope feels right and minimal. The three activation paths (--insecure CLI flag → QWEN_TLS_INSECURE env var → NODE_TLS_REJECT_UNAUTHORIZED=0 parity) cover the common cases. The security model is well thought through: project .env exclusion prevents untrusted repos from silently disabling TLS, separate cache entries for secure/insecure dispatchers prevent cross-contamination, and the warning is logged both to console and debug file. No unrelated changes or scope creep.

One observation: NODE_TLS_REJECT_UNAUTHORIZED=0 happens to work on the currently installed build for this particular endpoint, but the custom undici dispatcher's behavior with that env var is implementation-dependent and not guaranteed — having an explicit, documented --insecure path is still the right fix.

Moving on to code review. 🔍

中文说明

感谢 @TianYuan1024 的贡献!

模板完整 ✓ — 所有必填部分齐全,双语,含测试计划。

方向:解决了一个真实的痛点。用户将 Qwen Code 指向使用自签名证书的自建端点时没有逃生通道,因为自建 undici dispatcher(用于控制超时)并不可靠地遵循 NODE_TLS_REJECT_UNAUTHORIZED=0--insecure 参数在 CLI 工具中是标准做法(curl -kwget --no-check-certificate),需主动开启并附带 MITM 警告的安全姿态是正确的。Claude Code CHANGELOG 没有直接参考,但问题域(企业/实验室环境的自定义 TLS 信任)与 qwen-code 的用户群高度吻合。

方案:范围恰当且精简。三种激活路径(--insecure CLI 参数 → QWEN_TLS_INSECURE 环境变量 → NODE_TLS_REJECT_UNAUTHORIZED=0 兼容)覆盖了常见场景。安全模型考虑周全:项目 .env 排除防止不受信任的仓库静默禁用 TLS,secure/insecure dispatcher 分开缓存防止交叉污染,警告同时输出到控制台和调试文件。没有无关改动或范围蔓延。

一个观察:当前已安装版本对该特定端点恰好支持 NODE_TLS_REJECT_UNAUTHORIZED=0,但自建 undici dispatcher 对该环境变量的行为是实现相关的、不保证一致——有一个明确的、有文档说明的 --insecure 路径仍然是正确的修复。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Reviewed the diff against an independent proposal for this problem. The PR's approach matches what I would have built: a single isTlsVerificationDisabled() helper checking QWEN_TLS_INSECURE then NODE_TLS_REJECT_UNAUTHORIZED=0, applied to both the plain Agent (connect.rejectUnauthorized) and ProxyAgent (requestTls + proxyTls), with separate cache keys to prevent secure/insecure dispatcher cross-contamination.

No critical blockers found:

  • Security model is sound — both QWEN_TLS_INSECURE and NODE_TLS_REJECT_UNAUTHORIZED are in PROJECT_ENV_HARDCODED_EXCLUSIONS so a malicious repo's .env can't silently disable TLS.
  • The --insecure flag sets QWEN_TLS_INSECURE=1 and propagates to NODE_TLS_REJECT_UNAUTHORIZED=0 process-wide (with a clear warning), covering paths the custom dispatcher doesn't reach (Bun runtime, fallback fetch).
  • Error messaging in formatFetchErrorForUser() adapts: when --insecure is already active and a fetch still fails, the hint says "TLS verification is already disabled" instead of suggesting --insecure again.
  • All comments are why-explanations (security reasoning, cache separation rationale) — appropriate for code that handles TLS trust.

Unit Tests (all pass)

runtimeFetchOptions.test.ts:  65 passed (TLS opt-out: env var parsing, proxy vs direct, cache separation)
fetch.test.ts:                 4 passed (error message with/without --insecure)
config.test.ts:              241 passed (flag parsing, env propagation, warning behavior)
shared-env-keys.test.ts:       2 passed (security guard: .env exclusion)

Real-Scenario Testing

Tested against https://self-signed.badssl.com/ (public self-signed cert endpoint).

Baseline — curl confirms self-signed cert

$ curl -sv https://self-signed.badssl.com/ 2>&1 | head -20
* Host self-signed.badssl.com:443 was resolved.
*   Trying 104.154.89.105:443...
* Connected to self-signed.badssl.com (104.154.89.105) port 443
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: self-signed certificate
* Closing connection

Before (installed build, no --insecure)

$ qwen --auth-type openai --openaiBaseUrl https://self-signed.badssl.com --openaiApiKey sk-test -p 'say hi'
[API Error: Connection error. (cause: fetch failed)]

TLS rejection — no escape hatch in the installed version.

Before (installed build, NODE_TLS_REJECT_UNAUTHORIZED=0)

$ NODE_TLS_REJECT_UNAUTHORIZED=0 qwen --auth-type openai --openaiBaseUrl https://self-signed.badssl.com --openaiApiKey sk-test -p 'say hi'
(node:1084481) Warning: Setting the NODE_TLS_REJECT_UNAUTHORIZED environment variable to '0' makes TLS connections and HTTPS requests insecure by disabling certificate verification.
[API Error: 404 <html>...<center><h1>404 Not Found</h1></center>...<center>nginx/1.10.3 (Ubuntu)</center>

Gets past TLS (404 from nginx = server reached), but via an undocumented, process-wide Node.js env var with a generic warning. The custom dispatcher's behavior with this env var is implementation-dependent.

After (this PR, --insecure)

$ npm run dev -- --auth-type openai --openaiBaseUrl https://self-signed.badssl.com --openaiApiKey sk-test --insecure -p 'say hi'
WARNING: TLS certificate verification is disabled (--insecure / QWEN_TLS_INSECURE). All HTTPS connections in this process (API calls, OAuth, MCP servers, child processes) are vulnerable to man-in-the-middle attacks.
(node:1084924) Warning: Setting the NODE_TLS_REJECT_UNAUTHORIZED environment variable to '0' makes TLS connections and HTTPS requests insecure by disabling certificate verification.
[API Error: 404 <html>...<center><h1>404 Not Found</h1></center>...<center>nginx/1.10.3 (Ubuntu)</center>

TLS bypassed — request reaches the server (404 expected, badssl.com doesn't serve an OpenAI API). The explicit --insecure warning is clear about the blast radius.

After (this PR, no --insecure)

$ npm run dev -- --auth-type openai --openaiBaseUrl https://self-signed.badssl.com --openaiApiKey sk-test -p 'say hi'
[API Error: Connection error. (cause: fetch failed)]

Default behavior unchanged — TLS verification still rejects self-signed certs.

Verdict: The PR works exactly as described. --insecure provides a documented, discoverable escape hatch; default behavior is unchanged.

中文说明

代码审查

将 diff 与独立方案对比。PR 的方案与我预期的完全一致:单一 isTlsVerificationDisabled() 辅助函数先检查 QWEN_TLS_INSECURE 再检查 NODE_TLS_REJECT_UNAUTHORIZED=0,分别应用到 plain Agent(connect.rejectUnauthorized)和 ProxyAgent(requestTls + proxyTls),并使用独立缓存键防止 secure/insecure dispatcher 交叉污染。

未发现关键阻塞问题:

  • 安全模型健全——QWEN_TLS_INSECURENODE_TLS_REJECT_UNAUTHORIZED 都在 PROJECT_ENV_HARDCODED_EXCLUSIONS 中,恶意仓库的 .env 无法静默禁用 TLS。
  • --insecure 参数设置 QWEN_TLS_INSECURE=1 并传播为 NODE_TLS_REJECT_UNAUTHORIZED=0(进程级),覆盖自建 dispatcher 未涉及的路径(Bun 运行时、备用 fetch)。
  • formatFetchErrorForUser() 的错误信息会自适应:当 --insecure 已启用但 fetch 仍失败时,提示"TLS 验证已禁用"而非再次建议 --insecure

单元测试(全部通过)

runtimeFetchOptions.test.ts:  65 通过(TLS 可选退出:环境变量解析、代理 vs 直连、缓存分离)
fetch.test.ts:                 4 通过(有/无 --insecure 的错误信息)
config.test.ts:              241 通过(参数解析、环境变量传播、警告行为)
shared-env-keys.test.ts:       2 通过(安全守卫:.env 排除)

真实场景测试

使用 https://self-signed.badssl.com/(公共自签名证书端点)测试。

已安装版本(无 --insecure):[API Error: Connection error. (cause: fetch failed)] — TLS 被拒绝。
已安装版本 + NODE_TLS_REJECT_UNAUTHORIZED=0: 绕过 TLS(到达服务器,404),但通过未文档化的进程级环境变量。
PR 代码 + --insecure: 绕过 TLS(到达服务器,404),有明确的 MITM 警告。
PR 代码(无 --insecure): [API Error: Connection error. (cause: fetch failed)] — 默认行为不变。

结论:PR 完全按描述工作。--insecure 提供了有文档、可发现的逃生通道;默认行为不变。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Stepping back to look at the whole picture:

This PR solves a well-defined problem (no TLS escape hatch for self-signed endpoints behind the custom undici dispatcher) with a clean, minimal implementation. The security model is genuinely well thought through — the .env exclusion list, the separate dispatcher caches, the adaptive error messaging, and the explicit MITM warning all show attention to the blast radius of disabling certificate verification.

The before/after testing confirms the feature works as designed: --insecure gets the request past TLS with a clear warning; without it, verification rejects self-signed certs as before. All 312 unit tests across the four affected test files pass.

Two rounds of maintainer review have already sharpened this (the .env exclusion for NODE_TLS_REJECT_UNAUTHORIZED and the separate cache entries both came from review feedback), and @wenshao verified it end-to-end with a real self-signed cert setup on macOS.

This is a focused, well-tested feature PR that does one thing and does it right. No scope creep, no unnecessary abstractions, no drive-by refactors. If I had to maintain this in six months, the comments would tell me exactly why the security boundaries exist.

Approving. ✅

中文说明

回顾全局:这个 PR 用精简的实现解决了一个明确的问题(自建 undici dispatcher 下自签名端点没有 TLS 逃生通道)。安全模型真正做到了深思熟虑——.env 排除列表、独立 dispatcher 缓存、自适应错误信息、明确的 MITM 警告,都体现了对禁用证书验证影响范围的考量。

前后对比测试确认功能按设计工作:--insecure 以明确警告绕过 TLS;未启用时,验证仍拒绝自签名证书。四个相关测试文件的 312 个单元测试全部通过。

两轮维护者审查已经打磨过(NODE_TLS_REJECT_UNAUTHORIZED.env 排除和独立缓存条目都来自审查反馈),@wenshao 在 macOS 上用真实自签名证书做了端到端验证。

这是一个聚焦、经过充分测试的功能 PR,做好了一件事。没有范围蔓延、没有不必要的抽象、没有顺手重构。如果六个月后需要维护这些代码,注释会清楚地告诉我安全边界存在的原因。

批准 ✅

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 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Jun 29, 2026
@wenshao
wenshao added this pull request to the merge queue Jun 29, 2026
Merged via the queue into QwenLM:main with commit 35a8851 Jun 29, 2026
42 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.

Feature request: a CLI flag or environment variable to allow Qwen Code to ignore SSL errors

4 participants