Repository navigation
fix(cli): extend the #8663 loader denylist and harden its scrub lifecycle #8763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
92c525f
eaac7ba
abee0fc
e34df6d
46e2690
ade3833
b77fc00
eca8ad2
3b70728
146c448
28352d5
02fcc3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -724,7 +724,7 @@ For authentication-related variables (like `OPENAI_*`) and the recommended `.qwe | |
|
|
||
| > [!warning] | ||
| > | ||
| > **Loader-affecting variables are always rejected:** Variables that make a spawned Node.js process or OS loader execute an attacker-chosen file — `NODE_OPTIONS`, `npm_config_node_options` (and npm's config-file redirects `npm_config_userconfig`, `npm_config_globalconfig`, `npm_config_script_shell`, `npm_config_prefix`), `NODE_PATH`, `LD_PRELOAD`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `BASH_ENV`, `ZDOTDIR`, and exported bash function definitions (`BASH_FUNC_*`) — are never loaded from `.env` files (any scope, including `.qwen/.env` and user-level files) or from the top-level `settings.json` `env` section. A workspace-controlled value there could hijack module resolution for every subprocess Qwen Code spawns, so Qwen Code prints a warning when it rejects such a key (once per process, per key and source — in a multi-workspace daemon each workspace's rejection is reported separately). To use one of these variables, export it in the environment you launch Qwen Code from; sessions hosted by a `qwen serve` daemon deliberately do not inherit them, while direct editor (ACP) sessions and the plain CLI keep the exported value. Library _search_ paths (`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`) and the interactive-shell-only `ENV` are intentionally not on this list — rejecting them breaks mainstream toolchains (`ENV=production`, conda/CUDA library dirs) — but a project `.env` still cannot apply them on reload. This rejection applies to the top-level `env` section only: per-server `mcpServers[].env` and per-hook `hooks[].env` are intentionally scoped to that server or hook and still apply (both surfaces are gated by folder trust for workspace-provided configs). Separately, a project `.env` can never set `QWEN_CLI_ENTRY` (the daemon's session-process entrypoint), `NODE_EXTRA_CA_CERTS`, or `DEV` (the dev-harness launch marker); those stay settable from the shell environment or a user-level `.env`. Upgrade note: before this denylist existed, some of these keys could load from `.env` files or `settings.json` `env` on some paths; they are now rejected everywhere with a warning, and a `qwen serve` daemon no longer passes inherited values of them to session subprocesses. | ||
| > **Loader-affecting variables are always rejected:** Variables that make a spawned Node.js process or OS loader execute an attacker-chosen file — `NODE_OPTIONS`, `npm_config_node_options` (and npm's config-file redirects `npm_config_userconfig`, `npm_config_globalconfig`, `npm_config_script_shell`, `npm_config_prefix`), `NODE_PATH`, `OPENSSL_CONF` (dlopens an attacker OpenSSL engine at startup), `NODE_REPL_EXTERNAL_MODULE`, `npm_config_node_gyp`, `npm_config_init_module`, `LD_PRELOAD`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `BASH_ENV`, `ZDOTDIR`, and exported bash function definitions (`BASH_FUNC_*`) — are never loaded from `.env` files (any scope, including `.qwen/.env` and user-level files) or from the top-level `settings.json` `env` section. A workspace-controlled value there could hijack module resolution for every subprocess Qwen Code spawns, so Qwen Code prints a warning when it rejects such a key (once per process, per key and source — in a multi-workspace daemon each workspace's rejection is reported separately). To use one of these variables, export it in the environment you launch Qwen Code from; sessions hosted by a `qwen serve` daemon deliberately do not inherit them, while direct editor (ACP) sessions and the plain CLI keep the exported value. Library _search_ paths (`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`) and the interactive-shell-only `ENV` are intentionally not on this list — rejecting them breaks mainstream toolchains (`ENV=production`, conda/CUDA library dirs) — but a project `.env` still cannot apply them on reload. This rejection applies to the top-level `env` section only: per-server `mcpServers[].env` and per-hook `hooks[].env` are intentionally scoped to that server or hook and still apply (both surfaces are gated by folder trust for workspace-provided configs). Separately, a project `.env` can never set `QWEN_CLI_ENTRY` (the daemon's session-process entrypoint), `DEV` (the dev-harness launch marker), the TLS trust-anchor variables (`NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `SSL_CERT_DIR`, `CURL_CA_BUNDLE`, `REQUESTS_CA_BUNDLE`, `GIT_SSL_CAINFO` — an attacker CA there would enable MITM of the token-bearing traffic a session's `git`/`npm`/`pip`/`curl` calls make), the git command-execution variables (`GIT_SSH_COMMAND`, `GIT_EXTERNAL_DIFF`, `GIT_CONFIG_GLOBAL`, `GIT_CONFIG_SYSTEM`, `GIT_CONFIG_COUNT` and the numbered `GIT_CONFIG_KEY_<n>`/`GIT_CONFIG_VALUE_<n>` pairs — git runs these on any session `git` invocation), or the node-gyp interpreter-selection variables (`NODE_GYP_FORCE_PYTHON`, `npm_config_python`, `PYTHON` — run as the build Python during native-addon installs). Those stay settable from the shell environment or a user-level `.env`; unlike the loader list above they are rejected from project files only, so a value you export yourself is preserved. Upgrade note: before this denylist existed, some of these keys could load from `.env` files or `settings.json` `env` on some paths; they are now rejected everywhere with a warning, and a `qwen serve` daemon no longer passes inherited values of them to session subprocesses. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deferred to the follow-up round. Valid gap: 中文说明延迟到下一轮。缺口属实: |
||
|
|
||
| ### Environment Variables Table | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,6 +66,7 @@ const mockResolveProxyUrl = vi.hoisted(() => | |
| vi.fn((_cliProxy?: string, settingsProxy?: string) => settingsProxy), | ||
| ); | ||
| const mockWriteStderrLine = vi.hoisted(() => vi.fn()); | ||
| const mockWriteStderrLineSafe = vi.hoisted(() => vi.fn()); | ||
| const mockWriteStdoutLine = vi.hoisted(() => vi.fn()); | ||
| const mockSelectFirstModel = vi.hoisted(() => | ||
| vi.fn( | ||
|
|
@@ -219,7 +220,7 @@ vi.mock('@qwen-code/qwen-code-core', () => ({ | |
|
|
||
| vi.mock('../../utils/stdioHelpers.js', () => ({ | ||
| writeStderrLine: mockWriteStderrLine, | ||
| writeStderrLineSafe: vi.fn(), | ||
| writeStderrLineSafe: mockWriteStderrLineSafe, | ||
| writeStdoutLine: mockWriteStdoutLine, | ||
| })); | ||
|
|
||
|
|
@@ -2033,6 +2034,13 @@ describe('daemonWorkerCommand', () => { | |
|
|
||
| expect(process.env['NODE_OPTIONS']).toBeUndefined(); | ||
| expect(process.env['npm_config_node-options']).toBeUndefined(); | ||
| // Pin the channel-boundary breadcrumb, not just the removal: a refactor | ||
| // onto the silent scrubInheritedLoaderEnv variant deletes the keys the | ||
| // same way but drops the operator diagnostic — the reason the *AndReport* | ||
| // helper exists. | ||
| expect(mockWriteStderrLineSafe).toHaveBeenCalledWith( | ||
| expect.stringContaining('scrubbed inherited loader env vars'), | ||
|
Comment on lines
+2041
to
+2042
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deferred to a follow-up. Valid probe: the suite stays green with the PR's source change reverted, so it does not gate this PR's behavior. Adding a daemon-worker case exercising a key this PR adds to the denylist (e.g. 中文说明延迟到下一轮。探针有效:回退本 PR 的源码改动后该套件仍为绿色,因此它并未钉住本 PR 的行为。计划在下一轮增加一个使用本 PR 新增拒绝键(如 |
||
| ); | ||
| }); | ||
|
|
||
| it('scrubs daemon connection env when required env validation fails', async () => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deferred — needs a maintainer decision between your two options: (a) reword the docs to state that the new keys are rejected from the top-level
envsection of any-scopesettings.json, or (b) make the four settings.env gates scope-aware like the.envgate. (b) changes this PR's security semantics (a user-levelsettings.jsonvalue would start applying), so it is not a call for a review round to make silently. Recommendation: (a) — minimal and factually accurate. Which option should we take?中文说明
延迟处理——需要维护者在你的两个选项之间做决定:(a) 改写文档,说明新增键会从任意作用域的
settings.json顶层env段被拒绝;或 (b) 让四个 settings.env 门控像.env门控一样区分作用域。(b) 会改变本 PR 的安全语义(用户级settings.json的值将开始生效),因此不适合由评审轮次悄悄决定。建议选 (a)——最小且与事实一致。请问选哪个?