mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-14 01:32:40 -04:00
fix: only suggest a pnpm config set command for shell-safe keys (#12335)
The key embedded in the warning's suggested 'pnpm config set' command comes
from a repository-controlled .npmrc. The previous guard only suppressed the
example for keys containing a ${...} placeholder, but a shell also expands
$(...), backticks and $VAR inside double quotes — so a crafted key could turn
the suggested copy-paste command into command execution. The example is now
emitted only for keys made up entirely of shell-inert characters.
This commit is contained in:
6
.changeset/shell-safe-config-set.md
Normal file
6
.changeset/shell-safe-config-set.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/config": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Hardened the warning printed when a project `.npmrc` uses environment variables in registry/auth settings: the suggested `pnpm config set` command is now only included for keys made up of shell-inert characters. Because the key comes from a repository-controlled `.npmrc` and a shell expands `$(...)`, backticks, and `$VAR` even inside double quotes, a crafted key could otherwise have turned the suggested copy-paste command into command execution.
|
||||
@@ -90,12 +90,17 @@ export function hasEnvPlaceholder (value: string): boolean {
|
||||
|
||||
const DOCS_URL = 'https://pnpm.io/npmrc'
|
||||
|
||||
// A runnable `pnpm config set` example is only safe to suggest when the key has
|
||||
// no `${...}` placeholder — embedding such a key in a shell command would let
|
||||
// the shell expand it on copy-paste, producing a different key and possibly
|
||||
// leaking an env value into shell history.
|
||||
// The key embedded in the suggested `pnpm config set` command comes from a
|
||||
// repository-controlled .npmrc. A shell expands `$(...)`, backticks and `$VAR`
|
||||
// even inside double quotes, so suggesting a runnable command built from an
|
||||
// arbitrary key would turn this warning into a copy-paste command-injection
|
||||
// vector. Only emit the runnable example for keys made up entirely of
|
||||
// shell-inert characters — which covers every real registry/auth key
|
||||
// (`//host/:_authToken`, `@scope:registry`, `registry`, `https-proxy`, …).
|
||||
const SHELL_SAFE_KEY = /^[\w@.:/-]+$/
|
||||
|
||||
function configSetExample (key: string): string {
|
||||
return hasEnvPlaceholder(key) ? '' : ` (for example, run: pnpm config set "${key}" <value>)`
|
||||
return SHELL_SAFE_KEY.test(key) ? ` (for example, run: pnpm config set "${key}" <value>)` : ''
|
||||
}
|
||||
|
||||
function warnIgnoredRequestDestinationEnv (filePath: string, key: string, warnings: string[]): void {
|
||||
|
||||
@@ -317,6 +317,30 @@ describe('project .npmrc env expansion trust boundary', () => {
|
||||
expect(urlScopedWarning).toContain('~/.npmrc')
|
||||
})
|
||||
|
||||
test('the warning never embeds a shell-unsafe key in a runnable pnpm config set command', async () => {
|
||||
prepare()
|
||||
|
||||
// A malicious repository could craft a key with shell metacharacters; the
|
||||
// suggested copy-paste command must not become a command-injection vector.
|
||||
fs.writeFileSync('.npmrc', '//$(touch pwned)`id`/:_authToken=${PNPM_TEST_TOKEN}\n', 'utf8')
|
||||
fs.mkdirSync('user-home')
|
||||
fs.writeFileSync(path.resolve('user-home', '.npmrc'), '', 'utf8')
|
||||
|
||||
const { warnings } = await getConfig({
|
||||
cliOptions: {
|
||||
userconfig: path.resolve('user-home', '.npmrc'),
|
||||
},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
|
||||
const unsafeWarning = warnings.find((w) => w.includes('$(touch pwned)')) ?? ''
|
||||
expect(unsafeWarning).not.toBe('')
|
||||
expect(unsafeWarning).not.toContain('pnpm config set "')
|
||||
})
|
||||
|
||||
test('project .npmrc does not expand env variables in auth values', async () => {
|
||||
prepare()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user