fix(core): Sanitize internal daemon secrets from shell subprocess environments - #6606
fix(core): Sanitize internal daemon secrets from shell subprocess environments#6606jadelike-wine wants to merge 23 commits into
Conversation
…tive with ACP child Extract the env-scrub helper from acp-bridge into core as shared `scrubChildEnv` and `collectSensitiveShellEnvKeys`, and use both to strip secret-semantic env vars (API keys, tokens, credentials, passwords, private keys, …) before spawning the shell subprocess. The model's own commands run there — including auto-allowed read-only `printenv`/`env` — so inherititing the daemon's env verbatim would let a `printenv QWEN_SERVER_TOKEN` exfiltrate tokens straight into the tool result. The ACP child deliberately keeps a NARROW scrub set because it is itself a `qwen` agent that MUST inherit provider API keys to call models; the two paths share one primitive but NOT one denylist — unifying them would reintroduce the leak or break the child. `collectSensitiveShellEnvKeys` drives the shell path's BROAD set, parametrized on the live `process.env`.
…scrub primitive(QwenLM#6601) Drop the broad user-credential denylist (GH_TOKEN, AWS_*, NPM_TOKEN, …) from child-env scrubbing — those belong to normal shell/MCP workflows and were wrongly stripped. Now only daemon/internal Qwen keys are stripped, and the scrub/collect primitive is shared between core (ACP/shell/MCP) and desktop (session-tools-core / mcp client) subprocess paths.
|
Qwen precheck requires maintainer approval before automated triage/review. Head SHA: Reason:
A maintainer with write access can inspect the PR and manually request a run with |
Windows environment variables are case-insensitive, so PATH and Path can both exist. Normalize them before scrubbing so discovery + execution child processes receive a single merged PATH and never a split/partial value. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… subprocess env Untrusted scripts spawned by the desktop session tools were inheriting provider API keys. Block LLM/AWS/GitHub/Google/Stripe/NPM credential keys in the sanitized environment alongside existing daemon-internal scrubbing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… sandbox subprocess env" This reverts commit 1530d8b.
wenshao
left a comment
There was a problem hiding this comment.
[Critical] packages/desktop/packages/shared/src/mcp/validation.ts:309 — The production StdioClientTransport at line 309 uses unsanitized { ...processEnv, ...env } while the probe at line 271 (changed by this diff) correctly uses createSanitizedChildEnv. The MCP handshake subprocess inherits QWEN_SERVER_TOKEN.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 73 out of 73 changed files in this pull request and generated 2 comments.
Suppressed comments (3)
packages/desktop/packages/shared/src/mcp/validation.test.ts:117
GITHUB_TOKENis part of the desktop scrub denylist (seeDESKTOP_CHILD_CREDENTIAL_ENV_KEYS). If this test is meant to validate the scrubber is applied, it should assert the token is not present in the spawned env even when provided via overrides.
packages/desktop/packages/shared/src/mcp/validation.test.ts:145QWEN_API_KEYis part of the desktop scrub policy and should be removed case-insensitively on Windows. OncevalidateStdioMcpConnectionapplies overrides viacreateSanitizedChildEnv(process.env, env),Qwen_Api_Keyshould remain scrubbed (undefined) even if provided in overrides.
packages/desktop/packages/shared/src/mcp/validation.test.ts:113- After switching to
createSanitizedChildEnv(process.env, env), scrubbed keys (likeQWEN_SERVER_TOKEN/LLM_API_KEY) should remain absent even if provided inenvoverrides. These expectations currently assert the opposite, which would mask a regression in the scrubber contract.
This issue also appears in the following locations of the same file:
- line 114
- line 142
| // MCP child processes must not inherit daemon credentials. Reuse this | ||
| // exact env for both the probe spawn and the production stdio transport. | ||
| const sanitizedEnv = { ...createSanitizedChildEnv(process.env), ...env }; |
| env: { | ||
| ...createSanitizedChildEnv(process.env), | ||
| ...config.env, | ||
| } as Record<string, string>, |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 73 out of 73 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
packages/core/src/lsp/LspServerManager.ts:770
- These env builders now always pass an explicit, cloned env object to spawned LSP processes. On Windows, that can drop PATH entries when both PATH/Path variants exist unless you normalize first. Other spawn sites already do this via normalizePathEnvForWindows(process.env) before scrubbing (e.g. ToolRegistry). Consider normalizing in buildProcessEnv/buildCommandProbeEnv before calling scrubChildEnv to avoid commandExists()/LSP startup regressions on win32.
| // Combine the static denylist with a pattern-based sweep of the base | ||
| // env so QWEN_CUSTOM_API_KEY_* (and any future internal Qwen secret | ||
| // pattern) is stripped even if .env/settings.env re-introduced it | ||
| // after the daemon's boot-time self-scrub. Defense-in-depth: the | ||
| // daemon already removed these from process.env, but this is the | ||
| // final spawn boundary for stdio MCP children. | ||
| const scrubbed = new Set<string>(SCRUBBED_STDIO_ENV_KEYS); | ||
| for (const key of collectSensitiveShellEnvKeys(baseEnv)) { | ||
| scrubbed.add(key); | ||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 73 out of 73 changed files in this pull request and generated no new comments.
Suppressed comments (3)
packages/cli/src/serve/voice/resolve-voice-config.ts:124
- loadDaemonVoiceContext() builds ModelsConfig using baseEnv (without the credentialStore snapshot), but the config construction uses env for auth-type detection and CLI generation config resolution. This can make voice config resolution diverge from the merged env actually used for transcription (e.g., when credentials exist only in the credentialStore). Use mergedEnv when calling buildModelsConfig so both paths see the same effective env.
const models = buildModelsConfig(settings, baseEnv);
packages/core/src/lsp/LspServerManager.ts:770
- buildProcessEnv() now always returns an explicit env object (even when no overrides are provided). On Windows, passing an explicit env without normalizing PATH-like keys can lead to multiple case-variants (PATH/Path/...) being present and an unexpected effective PATH for spawned LSP processes. Consider normalizing PATH first (via normalizePathEnvForWindows) before scrubbing/merging overrides, similar to other spawn call sites.
packages/core/src/lsp/LspServerManager.ts:803 - buildCommandProbeEnv() also always returns an explicit env object now. For the same reason as buildProcessEnv(), consider normalizing PATH-like keys (PATH/Path/...) on Windows before scrubbing and applying the filtered overrides, so the probe spawn reliably finds the intended executable via PATH.
|
@qwen-code /resolve |
Reconcile the daemon-secret sanitization work with changes that landed on main: keep the post-scrub daemonRuntimeBaseEnv snapshot (clean by construction) while folding in main's memoryProjectScope injection, thread credentialStore into the hoisted loadSettings call in workspace-skills-status, and union the CredentialStore/Storage and credentials/memoryScopes imports that both sides added at the same sites.
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge: PR #6606 (daemon secret sanitization) ← origin/mainRoot cause
Textual or semantic
What is load-bearing
Not verifiedNo build/typecheck/lint/tests run. Statically: all unioned imports used; 中文说明根因: 文本还是语义: 关键承重点: 未验证:未运行 build/typecheck/lint/测试。已静态检查:并集 import 均被使用; |
What this PR does
Adds a shared environment sanitization utility for child processes and applies it to model-controlled execution paths.
The shell execution path now removes Qwen internal daemon secrets, such as QWEN_SERVER_TOKEN, before spawning child processes.
The sanitization scope intentionally only covers internal Qwen credentials. User-provided credentials required for normal workflows, such as GitHub, AWS, and npm authentication variables, remain available to shell commands.
Why it's needed
Shell subprocesses previously inherited the full daemon process environment through process.env.
This allowed model-controlled shell commands to access daemon-level credentials that should never be exposed outside the Qwen Code process boundary.
For example, QWEN_SERVER_TOKEN could be read from shell commands executed by the agent.
This change closes that defense-in-depth gap while preserving existing user workflows that rely on inherited third-party credentials.
Reviewer Test Plan
How to verify
export QWEN_SERVER_TOKEN=test-secret-token
printenv QWEN_SERVER_TOKEN
Expected:
The variable should not be visible in the shell subprocess environment.
export GH_TOKEN=test-github-token
Run:
printenv GH_TOKEN
Expected:
GH_TOKEN should still be available to shell commands.
Evidence (Before & After)
N/A - Non-user-visible security hardening change.
Tested on
Environment (optional)
Qwen Code v0.19.8
macOS arm64
Node.js v24.3.0
Verified with local CLI execution and unit tests.
Risk & Scope
Main risk or tradeoff:
The sanitization intentionally only removes internal Qwen daemon secrets. Third-party user credentials remain available to preserve shell workflows such as gh, aws, and npm commands.
Not validated / out of scope:
This PR does not change user credential handling or introduce permission prompts for shell environment access.
Breaking changes / migration notes:
Commands that previously relied on accessing Qwen internal daemon credentials from the shell environment will no longer work.
Linked Issues
Fixes #6601
中文说明
本 PR 做了什么
新增共享环境变量清理逻辑,并应用到受模型控制的子进程执行路径。
Shell 子进程启动前会移除 Qwen 内部 daemon 密钥,例如 QWEN_SERVER_TOKEN。
清理范围仅针对 Qwen 内部凭据,不影响用户正常使用 GitHub、AWS、npm 等需要环境变量认证的工作流。
为什么需要
之前 shell 子进程会完整继承 daemon 的 process.env。
这导致模型执行的 shell 命令可能读取 daemon 级别的认证信息。
例如 QWEN_SERVER_TOKEN 可能通过 shell 命令暴露。
本修改关闭该安全风险,同时保持用户现有工作流兼容。