Skip to content

fix(e2): exempt child-process env pass-through from harvesting - #492

Open
malinfossum wants to merge 1 commit into
NVIDIA:mainfrom
malinfossum:malinfossum/e2-child-process-env-passthrough
Open

fix(e2): exempt child-process env pass-through from harvesting#492
malinfossum wants to merge 1 commit into
NVIDIA:mainfrom
malinfossum:malinfossum/e2-child-process-env-passthrough

Conversation

@malinfossum

Copy link
Copy Markdown

Summary

E2 no longer fires on an os.environ copy whose only destination is a child process's env=. An environ copy that goes anywhere else is unchanged, and so are E1, E3E5, the regex fallback for unparsable Python, and every non-Python path.

Fixes #441.

Root cause

_analyze_python_environment_reads keys on the copy rather than on where the copy goes, so subprocess.run(cmd, env={**os.environ, "GIT_OPTIONAL_LOCKS": "0"}) reached the same emit() at the same HIGH severity and the same 0.6 confidence as a real harvester. The analyzer's docstring already excluded this case — a full mapping copy is a harvesting signal "unlike a targeted single-key lookup or passing os.environ through to a child process" — but only the first half was implemented.

The fix collects, per file, the expressions passed as env= to a known process launcher (subprocess.run / call / check_call / check_output / Popen, asyncio.create_subprocess_exec / _shell) plus the plain names bound to them, and skips those nodes at emit time. Two shapes are covered: the mapping written inline at the call site, and one built on an earlier line and passed by name.

I kept this to an allowlist rather than exempting any env= keyword, so a call to an arbitrary function named with an env= argument is not a way to silence E2.

Validation

A skill with the issue's three benign variants plus one real harvester (requests.post(url, json=dict(os.environ))), scanned with --no-llm:

before after
variants.py:8 env={**os.environ, ...} E2 HIGH conf 0.6 not flagged
variants.py:13 env = os.environ.copy() E2 HIGH conf 0.6 not flagged
harvester.py:7 dict(os.environ)requests.post E2 HIGH conf 0.6 E2 HIGH conf 0.6
score 69 54

The subprocess calls themselves still surface as AST4, so the behavior is not hidden — only the harvesting claim about it is withdrawn.

Three tests added to TestRunStaticPatternsDataExfiltration, written before the fix and confirmed failing against main: one per benign shape, plus a guard that an environ copy bound to a name and sent to requests.post still fires.

  • pytest tests/nodes/analyzers/test_static_patterns.py -k e2 — 7 passed
  • pytest -m "not integration and not provider" tests/ — 3954 passed, 26 skipped, 4 xfailed, 22 failed
  • ruff check src/ tests/ — clean
  • ruff format --check src/ tests/ — clean

The 22 failures are pre-existing on a Windows host and unrelated to this change: I ran the same four files on main with this branch stashed and got the identical 22 (test_build_context.py symlink and secure-open cases, test_compare_scan_accuracy.py — which is #485test_create_github_release.py, and one test_input_handler.py case). No test outside test_static_patterns.py changes state with this patch applied.

Scope

This fixes the E2 precision slice only. It does not touch E2's severity or confidence values, the #329 broadening that made the rule match {**os.environ, ...} in the first place, or E1/taint coverage of credential flows to network sinks.

E2 fired HIGH at 0.6 confidence on `subprocess.run(cmd, env={**os.environ,
...})` and on an `os.environ.copy()` bound to a name and passed as `env=`,
which is the standard way to hand an environment to a child process. A real
harvester and the most common benign idiom were indistinguishable in the
report.

The analyzer's own docstring already excluded this case: a full mapping copy
is a harvesting signal "unlike a targeted single-key lookup or passing
os.environ through to a child process". The code did not implement the second
half, because it keyed on the copy rather than on where the copy goes.

Collect the expressions passed as `env=` to a known process launcher, plus the
names bound to them, and skip those at emit time. An environ copy that goes
anywhere else still fires, and network and execution sinks remain the
behavioral taint analyzer's job.

Fixes NVIDIA#441

Signed-off-by: Malin Fossum <malinfossum.dev@proton.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant