From 4049296ca8eef2067517d8cfd5ef5c0e615daa10 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Fri, 16 Oct 2026 20:08:01 -0700 Subject: [PATCH] [Fix] `nvm_die_on_prefix`: never let a project `.npmrc` choose which npmrc file `--delete-prefix` edits `nvm use --delete-prefix`, which nvm itself recommends when it finds a `prefix` or `globalconfig` setting, ran `npm config delete` from the current directory. For a project `.npmrc`, it ran a bare `npm config delete prefix`/`globalconfig`, so npm loaded that untrusted file first, and its `userconfig`, `globalconfig`, or `location=global` keys picked which file npm rewrote, deleted, or chmodded to 0666 - eg, `~/.bashrc` or `~/.aws/credentials`. The builtin and user branches pinned `--userconfig`, but still loaded the project file, so a project `location=global` plus an indented ` globalconfig=` (which nvm's check does not match) redirected those writes the same way. `npm config delete` never edits the project file anyway, so that branch could not do what it claimed. For the project npmrc, no longer run npm at all: with `--delete-prefix`, warn that project npmrc files are not modified and must be edited by hand, and continue, as before. For the builtin, global, and user npmrc files, pass `--prefix` set to the version directory, so npm does not walk up from the current directory to load the project npmrc, and its global config resolves to that version's `etc/npmrc`. Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-WYRTKBQ5. --- nvm.sh | 21 +++++++----- test/fast/Unit tests/nvm_die_on_prefix | 46 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/nvm.sh b/nvm.sh index 1421d70a..7044ae77 100755 --- a/nvm.sh +++ b/nvm.sh @@ -4273,13 +4273,17 @@ nvm_die_on_prefix() { # # if any of them have a `prefix`, fail. # if any have `globalconfig`, fail also, just in case, to avoid spidering configs. + # + # when deleting, pass `--prefix` so npm does not walk up from `$PWD` and load the project npmrc: + # it is untrusted, and its `userconfig`/`globalconfig`/`location` would choose which file npm rewrites. + # `npm config delete` never edits the project npmrc itself, so that one is left for the user to edit. local NVM_NPM_BUILTIN_NPMRC NVM_NPM_BUILTIN_NPMRC="${NVM_VERSION_DIR}/lib/node_modules/npm/npmrc" if nvm_npmrc_bad_news_bears "${NVM_NPM_BUILTIN_NPMRC}"; then if [ "_${NVM_DELETE_PREFIX}" = "_1" ]; then - npm config --loglevel=warn delete prefix --userconfig="${NVM_NPM_BUILTIN_NPMRC}" - npm config --loglevel=warn delete globalconfig --userconfig="${NVM_NPM_BUILTIN_NPMRC}" + npm config --loglevel=warn delete prefix --userconfig="${NVM_NPM_BUILTIN_NPMRC}" --prefix="${NVM_VERSION_DIR}" + npm config --loglevel=warn delete globalconfig --userconfig="${NVM_NPM_BUILTIN_NPMRC}" --prefix="${NVM_VERSION_DIR}" else nvm_err "Your builtin npmrc file ($(nvm_sanitize_path "${NVM_NPM_BUILTIN_NPMRC}"))" nvm_err 'has a `globalconfig` and/or a `prefix` setting, which are incompatible with nvm.' @@ -4292,8 +4296,8 @@ nvm_die_on_prefix() { NVM_NPM_GLOBAL_NPMRC="${NVM_VERSION_DIR}/etc/npmrc" if nvm_npmrc_bad_news_bears "${NVM_NPM_GLOBAL_NPMRC}"; then if [ "_${NVM_DELETE_PREFIX}" = "_1" ]; then - npm config --global --loglevel=warn delete prefix - npm config --global --loglevel=warn delete globalconfig + npm config --global --loglevel=warn delete prefix --prefix="${NVM_VERSION_DIR}" + npm config --global --loglevel=warn delete globalconfig --prefix="${NVM_VERSION_DIR}" else nvm_err "Your global npmrc file ($(nvm_sanitize_path "${NVM_NPM_GLOBAL_NPMRC}"))" nvm_err 'has a `globalconfig` and/or a `prefix` setting, which are incompatible with nvm.' @@ -4306,8 +4310,8 @@ nvm_die_on_prefix() { NVM_NPM_USER_NPMRC="${HOME}/.npmrc" if nvm_npmrc_bad_news_bears "${NVM_NPM_USER_NPMRC}"; then if [ "_${NVM_DELETE_PREFIX}" = "_1" ]; then - npm config --loglevel=warn delete prefix --userconfig="${NVM_NPM_USER_NPMRC}" - npm config --loglevel=warn delete globalconfig --userconfig="${NVM_NPM_USER_NPMRC}" + npm config --loglevel=warn delete prefix --userconfig="${NVM_NPM_USER_NPMRC}" --prefix="${NVM_VERSION_DIR}" + npm config --loglevel=warn delete globalconfig --userconfig="${NVM_NPM_USER_NPMRC}" --prefix="${NVM_VERSION_DIR}" else nvm_err "Your user’s .npmrc file ($(nvm_sanitize_path "${NVM_NPM_USER_NPMRC}"))" nvm_err 'has a `globalconfig` and/or a `prefix` setting, which are incompatible with nvm.' @@ -4320,8 +4324,9 @@ nvm_die_on_prefix() { NVM_NPM_PROJECT_NPMRC="$(nvm_find_project_dir)/.npmrc" if nvm_npmrc_bad_news_bears "${NVM_NPM_PROJECT_NPMRC}"; then if [ "_${NVM_DELETE_PREFIX}" = "_1" ]; then - npm config --loglevel=warn delete prefix - npm config --loglevel=warn delete globalconfig + nvm_err "Your project npmrc file ($(nvm_sanitize_path "${NVM_NPM_PROJECT_NPMRC}"))" + nvm_err 'has a `globalconfig` and/or a `prefix` setting, which are incompatible with nvm.' + nvm_err 'Project npmrc files are not modified by `--delete-prefix`; edit that file to unset it.' else nvm_err "Your project npmrc file ($(nvm_sanitize_path "${NVM_NPM_PROJECT_NPMRC}"))" nvm_err 'has a `globalconfig` and/or a `prefix` setting, which are incompatible with nvm.' diff --git a/test/fast/Unit tests/nvm_die_on_prefix b/test/fast/Unit tests/nvm_die_on_prefix index 11237fde..44691446 100755 --- a/test/fast/Unit tests/nvm_die_on_prefix +++ b/test/fast/Unit tests/nvm_die_on_prefix @@ -209,6 +209,52 @@ Run \`foo\` to unset it." [ "_$CAPTURED_EXIT_CODE" = "_10" ] || die "'nvm_die_on_prefix 0 foo' with user .npmrc that has globalconfig did not exit with 10; got '$CAPTURED_EXIT_CODE'" ) +# "delete the prefix": npm must not load the (untrusted) project npmrc, which could otherwise choose +# the file npm rewrites via `userconfig`/`globalconfig`/`location`, and the project npmrc is never edited +( + DELETE_DIR="${TEST_DIR}/delete-prefix" + DELETE_VERSION_DIR="${DELETE_DIR}/version" + DELETE_PROJECT_DIR="${DELETE_DIR}/project" + DELETE_HOME="${DELETE_DIR}/home" + mkdir -p "${DELETE_VERSION_DIR}/etc" "${DELETE_VERSION_DIR}/lib/node_modules/npm" "${DELETE_PROJECT_DIR}" "${DELETE_HOME}" + export HOME="${DELETE_HOME}" + echo 'prefix=garbage' > "${DELETE_VERSION_DIR}/lib/node_modules/npm/npmrc" + echo 'prefix=garbage' > "${DELETE_VERSION_DIR}/etc/npmrc" + echo 'prefix=garbage' > "${DELETE_HOME}/.npmrc" + + cd "${DELETE_PROJECT_DIR}" || die 'could not cd to the project dir' + touch package.json + PROJECT_NPMRC_CONTENTS="prefix=garbage +userconfig=${DELETE_HOME}/victim +location=global +globalconfig=${DELETE_HOME}/victim" + echo "${PROJECT_NPMRC_CONTENTS}" > .npmrc + + NPM_CALLS="${DELETE_DIR}/npm-calls" + npm() { + echo "$*" >> "${NPM_CALLS}" + } + + try_err nvm_die_on_prefix 1 foo "${DELETE_VERSION_DIR}" + + EXPECTED_OUTPUT="Your project npmrc file ($(nvm_sanitize_path "${DELETE_PROJECT_DIR}")/.npmrc) +has a \`globalconfig\` and/or a \`prefix\` setting, which are incompatible with nvm. +Project npmrc files are not modified by \`--delete-prefix\`; edit that file to unset it." + [ "_$CAPTURED_STDERR" = "_$EXPECTED_OUTPUT" ] || die "'nvm_die_on_prefix 1 foo' with a project .npmrc that has prefix did not warn with '$EXPECTED_OUTPUT'; got '$CAPTURED_STDERR'" + [ "_$CAPTURED_EXIT_CODE" = "_0" ] || die "'nvm_die_on_prefix 1 foo' with a project .npmrc that has prefix did not exit with 0; got '$CAPTURED_EXIT_CODE'" + + EXPECTED_OUTPUT="config --loglevel=warn delete prefix --userconfig=${DELETE_VERSION_DIR}/lib/node_modules/npm/npmrc --prefix=${DELETE_VERSION_DIR} +config --loglevel=warn delete globalconfig --userconfig=${DELETE_VERSION_DIR}/lib/node_modules/npm/npmrc --prefix=${DELETE_VERSION_DIR} +config --global --loglevel=warn delete prefix --prefix=${DELETE_VERSION_DIR} +config --global --loglevel=warn delete globalconfig --prefix=${DELETE_VERSION_DIR} +config --loglevel=warn delete prefix --userconfig=${DELETE_HOME}/.npmrc --prefix=${DELETE_VERSION_DIR} +config --loglevel=warn delete globalconfig --userconfig=${DELETE_HOME}/.npmrc --prefix=${DELETE_VERSION_DIR}" + OUTPUT="$(cat "${NPM_CALLS}")" + [ "_$OUTPUT" = "_$EXPECTED_OUTPUT" ] || die "'nvm_die_on_prefix 1 foo' did not run npm with a pinned \`--prefix\` (and never for the project npmrc): expected '$EXPECTED_OUTPUT'; got '$OUTPUT'" + + [ "_$(cat .npmrc)" = "_${PROJECT_NPMRC_CONTENTS}" ] || die "'nvm_die_on_prefix 1 foo' edited the project .npmrc; got '$(cat .npmrc)'" +) || die "'nvm_die_on_prefix 1 foo' let a project .npmrc influence npm" + # nvm_npmrc_bad_news_bears matches `^(prefix|globalconfig) *=` NPMRC="${TEST_DIR}/bad-news.npmrc" printf 'prefix=x\n' > "${NPMRC}"