Skip to content

finding: 8 packages/cli/test spawners pass no env at all, so the child inherits the vitest worker environment verbatim — the purer form of #11341's leak, and the new gate is silent on it #11595

Description

@os-zhuang

Surfaced while implementing #11341 (the childEnv() enforcement gate). Filed rather than folded in: #11341's declared surface is the check script, its self-test, its wiring and its baseline, and the fix here would be edits to eight test files — including two in a neighbourhood another card held in flight at the time.

The gap

#11341's gate refuses a bulk process.env reference in a spawner file under packages/cli/test/** — a spread, an Object.assign source, an alias. It says nothing about a spawn that passes no env option at all, and that child gets process.env verbatim: TEST=true, VITEST=true, VITEST_WORKER_ID, the lot.

That is the same leak in a purer form. { ...process.env, NO_COLOR: '1' } at least lets the author see the inheritance they are asking for; omitting env inherits everything with nothing on the page to read.

Measured on origin/main @ 2dc0a770b

Eight call sites, derived by walking every spawner file's AST for calls to a node:child_process binding (or a promisify() wrapper of one) whose options object carries no env key:

packages/cli/test/authoring-rule-command-parity.test.ts:192 execFileSync
packages/cli/test/cloud-login-json-ndjson.e2e.test.ts:310 execFileSync
packages/cli/test/emit-json-pipe.test.ts:62 execFileP (promisify(execFile))
packages/cli/test/emit-json-pipe.test.ts:144 spawn
packages/cli/test/emit-json-pipe.test.ts:194 execFileP
packages/cli/test/format-zod-union.test.ts:176 execFileSync
packages/cli/test/login-json-ndjson.e2e.test.ts:298 execFileSync
packages/cli/test/metadata-type-schema-gate.test.ts:185 execFileSync

None of these is an active false green today. Every one spawns tsx on a probe script or a non-serve CLI command, so nothing in the child reads TEST or VITEST — the same reading #11341 recorded for its own fifteen. The gap is latent and structural, exactly as #11267 was before someone tried to write a security-posture assertion against it.

Why #11341 left it out rather than widening

Stated in that gate's header as a named false negative, not discovered afterwards. The reason is that closing it is a decision about the convention's reach, not about the defect:

childEnv() was written for spawned CLI children whose environment matters. Requiring it at execFileSync(TSX, [probe], { cwd }) in an in-process-flavoured unit test asserts that every child in this directory owes the convention — which may well be right, but it is a widening of childEnv()'s contract and it belongs to whoever owns that contract. A gate that reaches past its own convention is how a gate gets carved out.

What closing it would look like

Two halves, and the second is the one that needs the ruling:

  1. Extend scripts/check-cli-test-child-env.mjs to treat a spawn call with no env option as a finding. Mechanically small — the gate already resolves spawn bindings and their promisify() wrappers; the call-anchored pass exists in its measurement history and was set aside for the site-anchoring reason its header gives.
  2. Decide whether the eight above are repairs or carve-outs. A probe script that reads nothing from the environment arguably wants childEnv() anyway (it costs nothing and removes the question); a format-zod-union fixture spawning tsx may be better declared than changed.

Either outcome needs the eight baselined first if the gate lands before the repairs, for the same reason #11341's did.

Explicitly not this issue

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions