Summary
A saved concrete Bash(...) allow rule can be matched after Qwen strips leading environment assignments even when those assignments change the runtime semantics of the allowed program and cause additional code execution.
This does not require command substitution, backticks, $(), or another shell-side execution primitive.
For example, with the concrete allow rule:
this command can be normalized for permission matching to npm --version:
NODE_OPTIONS=--require=/tmp/qwen-preload.cjs npm --version
while Node/npm still interprets NODE_OPTIONS and loads /tmp/qwen-preload.cjs before the trusted main command runs.
The same underlying issue applies to other environment variables whose values alter executable startup behavior, such as Git's GIT_CONFIG_* environment configuration.
Harmless reproduction: NODE_OPTIONS
Assume the user has explicitly allowed only:
Create a harmless preload file:
// /tmp/qwen-preload.cjs
require('fs').writeFileSync('/tmp/qwen-node-options-poc', 'executed');
Then run:
NODE_OPTIONS=--require=/tmp/qwen-preload.cjs npm --version
Runtime verification outside Qwen shows that npm still succeeds while the preload runs first:
npm stdout: 10.9.2
returncode: 0
NODE_OPTIONS side effect: true
marker: executed
Expected behavior
Because the leading environment assignment changes the startup behavior of the allowed executable and introduces additional code execution, Qwen should require confirmation rather than treating it as equivalent to plain:
Actual behavior
matchesCommandPattern() normalizes the command through stripLeadingVariableAssignments() before comparing it with the saved Bash rule.
The helper removes every leading token matching:
so the permission matcher can reduce:
NODE_OPTIONS=--require=/tmp/qwen-preload.cjs npm --version
to:
and match the saved concrete rule.
Second reproduction: Git environment configuration
This is not Node-specific. Git accepts runtime configuration through environment variables.
A harmless executable fsmonitor hook can be prepared, for example:
#!/bin/sh
printf 'executed' > /tmp/qwen-git-env-poc
printf '2\n'
Then:
GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0=core.fsmonitor \
GIT_CONFIG_VALUE_0=/tmp/fsmonitor.sh \
git status --short
was verified to execute the configured program while git status itself still returns successfully:
git returncode: 0
git stdout:
git stderr:
core.fsmonitor side effect: true
marker: executed
A saved concrete rule such as:
therefore does not fully describe the execution Qwen is authorizing if arbitrary leading environment assignments are discarded before matching.
Root cause
matchesCommandPattern() calls stripLeadingVariableAssignments() before applying the saved command rule.
That normalization was introduced so benign prefixes such as:
PYTHONPATH=/tmp/lib python3 script.py
can still match a rule for the underlying command.
However, the normalization assumes that a syntactically static NAME=value assignment is semantically inert. That is not generally true.
Environment variables can be interpreted by the launched executable, runtime, loader, or shell startup machinery and can cause code execution before or during the trusted command.
This is especially important because the shell AST safety layer already treats commands with environment assignments conservatively (unknown / confirmation path), but an explicit Bash allow rule can bypass that default analysis once the normalized command matches.
The virtual shell-operation analysis also strips leading environment-assignment tokens before analyzing the main command, so it does not add a separate safety floor for these startup semantics.
Why this matters
Concrete allow rules are intended to represent an exact trust decision by the user. In AUTO mode, broad interpreter/package-manager allow rules may be removed as dangerous, but concrete rules such as Bash(npm --version) or Bash(npm test) are intentionally preserved because the user explicitly trusted that command.
Stripping arbitrary environment assignments widens that trust boundary from:
to effectively:
run this command under arbitrary process/runtime environment overrides
which can include additional executable code.
This is related to, but distinct from, #10192.
#10192 requires shell execution semantics inside the assignment itself, for example:
X=`command` npm --version
The obvious fix there is to preserve matching for simple static NAME=value prefixes while rejecting assignments containing command/process substitution.
That is insufficient here: the assignments in these reproductions are completely static strings. The extra execution happens because the target runtime/application interprets their values.
For example:
NODE_OPTIONS=--require=/tmp/qwen-preload.cjs npm --version
contains no shell command substitution at all.
Suggested fix
Do not treat arbitrary environment assignments as permission-neutral when matching concrete Bash allow rules.
Possible approaches:
- Keep env-prefixed commands at
ask unless the assignment names are explicitly known to be safe for the target command.
- Maintain a denylist of execution-sensitive environment variables as a defense-in-depth measure, including runtime/loader/startup variables, but avoid relying on a denylist alone because executable-specific environment semantics are open-ended.
- More conservatively, require the saved rule to include the environment prefix when environment assignments are present instead of stripping them from the trust decision.
- Preserve the current normalization only for rules or modes where widening the command identity is explicitly intended, not for concrete auto-approval grants.
Regression tests should cover at least:
against:
npm --version # allow
FOO=bar npm --version # policy-dependent
NODE_OPTIONS=--require=/tmp/preload.cjs npm --version # must not silently auto-allow
and an equivalent GIT_CONFIG_* case.
Severity
I believe this is High / P1 because it crosses the confirmation boundary of a saved concrete Bash allow rule and permits additional code execution without changing the trusted main command.
It is not zero-precondition: a matching saved Bash allow rule must already exist.
Summary
A saved concrete
Bash(...)allow rule can be matched after Qwen strips leading environment assignments even when those assignments change the runtime semantics of the allowed program and cause additional code execution.This does not require command substitution, backticks,
$(), or another shell-side execution primitive.For example, with the concrete allow rule:
this command can be normalized for permission matching to
npm --version:while Node/npm still interprets
NODE_OPTIONSand loads/tmp/qwen-preload.cjsbefore the trusted main command runs.The same underlying issue applies to other environment variables whose values alter executable startup behavior, such as Git's
GIT_CONFIG_*environment configuration.Harmless reproduction: NODE_OPTIONS
Assume the user has explicitly allowed only:
Create a harmless preload file:
Then run:
Runtime verification outside Qwen shows that npm still succeeds while the preload runs first:
Expected behavior
Because the leading environment assignment changes the startup behavior of the allowed executable and introduces additional code execution, Qwen should require confirmation rather than treating it as equivalent to plain:
Actual behavior
matchesCommandPattern()normalizes the command throughstripLeadingVariableAssignments()before comparing it with the saved Bash rule.The helper removes every leading token matching:
so the permission matcher can reduce:
to:
and match the saved concrete rule.
Second reproduction: Git environment configuration
This is not Node-specific. Git accepts runtime configuration through environment variables.
A harmless executable fsmonitor hook can be prepared, for example:
Then:
was verified to execute the configured program while
git statusitself still returns successfully:A saved concrete rule such as:
therefore does not fully describe the execution Qwen is authorizing if arbitrary leading environment assignments are discarded before matching.
Root cause
matchesCommandPattern()callsstripLeadingVariableAssignments()before applying the saved command rule.That normalization was introduced so benign prefixes such as:
can still match a rule for the underlying command.
However, the normalization assumes that a syntactically static
NAME=valueassignment is semantically inert. That is not generally true.Environment variables can be interpreted by the launched executable, runtime, loader, or shell startup machinery and can cause code execution before or during the trusted command.
This is especially important because the shell AST safety layer already treats commands with environment assignments conservatively (
unknown/ confirmation path), but an explicit Bash allow rule can bypass that default analysis once the normalized command matches.The virtual shell-operation analysis also strips leading environment-assignment tokens before analyzing the main command, so it does not add a separate safety floor for these startup semantics.
Why this matters
Concrete allow rules are intended to represent an exact trust decision by the user. In AUTO mode, broad interpreter/package-manager allow rules may be removed as dangerous, but concrete rules such as
Bash(npm --version)orBash(npm test)are intentionally preserved because the user explicitly trusted that command.Stripping arbitrary environment assignments widens that trust boundary from:
to effectively:
which can include additional executable code.
Relation to #10192
This is related to, but distinct from, #10192.
#10192 requires shell execution semantics inside the assignment itself, for example:
X=`command` npm --versionThe obvious fix there is to preserve matching for simple static
NAME=valueprefixes while rejecting assignments containing command/process substitution.That is insufficient here: the assignments in these reproductions are completely static strings. The extra execution happens because the target runtime/application interprets their values.
For example:
contains no shell command substitution at all.
Suggested fix
Do not treat arbitrary environment assignments as permission-neutral when matching concrete Bash allow rules.
Possible approaches:
askunless the assignment names are explicitly known to be safe for the target command.Regression tests should cover at least:
against:
and an equivalent
GIT_CONFIG_*case.Severity
I believe this is High / P1 because it crosses the confirmation boundary of a saved concrete Bash allow rule and permits additional code execution without changing the trusted main command.
It is not zero-precondition: a matching saved Bash allow rule must already exist.