1
0
mirror of https://github.com/nvm-sh/nvm.git synced 2026-10-26 00:14:32 +08:00

[Fix] nvm.sh: detect NVM_DIR without $0/$_; fall back to the default install location instead of the working directory

When `nvm.sh` is sourced with `NVM_DIR` unset,
it derived `NVM_DIR` from the sourced file in bash
(via `BASH_SOURCE`),
and otherwise from `dirname "${_:-$0}"`.
In shells that do not set `BASH_SOURCE`
(dash, sh, ksh, and zsh under `emulate sh` or without `FUNCTION_ARGZERO`),
`$0` is the shell's or the calling script's name and `$_` is whatever was inherited,
so `NVM_DIR` became an unrelated directory:
for `sh -c '. ~/.nvm/nvm.sh'`,
as run by `system(3)` or `popen(3)`,
that is the working directory,
whose `alias/default` and `versions/` nvm then used,
and from which `nvm cache clear` and friends would operate.

Detection now uses only reliable sources:
`BASH_SOURCE` in bash,
`${(%):-%x}` in zsh
(which names the sourced file regardless of options),
and `${.sh.file}` in ksh93
(probed in a subshell first, since other shells with `KSH_VERSION` reject the syntax)
A detected source that is not a regular file
(eg, `/dev/fd/63` from `. <(…)`)
is ignored.
Otherwise, nvm uses the default install location that `install.sh` uses
(`$XDG_CONFIG_HOME/nvm`, else `~/.nvm`)
when it contains `nvm.sh`, and if not,
it prints an error asking for `NVM_DIR` to be set and returns 1,
instead of guessing.
Setups that export `NVM_DIR` before sourcing `nvm.sh`,
as `install.sh` and the README do, are unaffected

Discovered by Anthropic's OSS Scanner, as ANT-2026-8RIT0MGO; the scanner reported it as a vulnerability, but it is not one.
This commit is contained in:
Jordan Harband
2026-10-18 23:16:03 -07:00
parent c07e8722a1
commit 35d6e5a000
3 changed files with 97 additions and 6 deletions

View File

@@ -909,7 +909,7 @@ Nota bene: Avoid running nvm while the tests are running.
nvm exposes the following environment variables:
- `NVM_DIR` - nvm's installation directory.
- `NVM_DIR` - nvm's installation directory. If it is not set when `nvm.sh` is sourced, bash, zsh, and ksh93 use the directory containing `nvm.sh`; other shells (like dash) cannot tell where a sourced file lives, so they use the default install location (`$XDG_CONFIG_HOME/nvm` when `XDG_CONFIG_HOME` is set, otherwise `~/.nvm`). If nvm is installed elsewhere, set `NVM_DIR` before sourcing `nvm.sh`.
- `NVM_BIN` - where node, npm, and global packages for the active version of node are installed.
- `NVM_INC` - node's include file directory (useful for building C/C++ addons for node).
- `NVM_CD_FLAGS` - used to maintain compatibility with zsh.

35
nvm.sh
View File

@@ -10,9 +10,6 @@
# shellcheck disable=SC2039,SC2016,SC2001,SC3043
{ # this ensures the entire script is downloaded #
# shellcheck disable=SC3028
NVM_SCRIPT_SOURCE="${_:-}"
nvm_is_zsh() {
[ -n "${ZSH_VERSION-}" ]
}
@@ -589,12 +586,40 @@ fi
# Auto detect the NVM_DIR when not set
if [ -z "${NVM_DIR-}" ]; then
NVM_SCRIPT_SOURCE=''
# shellcheck disable=SC2128
if [ -n "${BASH_SOURCE-}" ]; then
NVM_SCRIPT_SOURCE="${BASH_SOURCE}"
elif nvm_is_zsh; then
# `%x` names the file being sourced even when `$0` does not, eg under `emulate sh`
eval 'NVM_SCRIPT_SOURCE="${(%):-%x}"'
elif [ -n "${KSH_VERSION-}" ] && (eval ': "${.sh.file}"') 2>/dev/null; then
# only ksh93 has `.sh.file`; other shells abort on the syntax, so it is probed in a subshell first
eval 'NVM_SCRIPT_SOURCE="${.sh.file}"'
fi
if [ -n "${NVM_SCRIPT_SOURCE}" ] && [ -f "${NVM_SCRIPT_SOURCE}" ]; then
# shellcheck disable=SC2086
NVM_DIR="$(nvm_cd ${NVM_CD_FLAGS} "$(dirname "${NVM_SCRIPT_SOURCE}")" >/dev/null && \pwd)"
else
# other shells (eg, dash) cannot tell where a sourced file lives (`$0` names the shell or the calling script),
# and a file sourced from a pipe (eg, `/dev/fd/63`) has no directory, so use the default install location, as `install.sh` does
if [ -n "${XDG_CONFIG_HOME-}" ]; then
NVM_DIR="${XDG_CONFIG_HOME}/nvm"
else
NVM_DIR="${HOME-}/.nvm"
fi
if [ -f "${NVM_DIR}/nvm.sh" ]; then
# shellcheck disable=SC2086
NVM_DIR="$(nvm_cd ${NVM_CD_FLAGS} "${NVM_DIR}" >/dev/null && \pwd)"
else
NVM_DIR=''
fi
fi
if [ -z "${NVM_DIR}" ]; then
unset NVM_DIR NVM_SCRIPT_SOURCE 2>/dev/null
nvm_err 'nvm: unable to detect $NVM_DIR; set it to the directory containing nvm.sh before sourcing it'
return 1
fi
# shellcheck disable=SC2086
NVM_DIR="$(nvm_cd ${NVM_CD_FLAGS} "$(dirname "${NVM_SCRIPT_SOURCE:-$0}")" >/dev/null && \pwd)"
export NVM_DIR
else
# https://unix.stackexchange.com/a/198289

View File

@@ -0,0 +1,66 @@
#!/bin/sh
\. ../common.sh
cleanup() {
[ -n "${TMP_DIR-}" ] && rm -rf "${TMP_DIR}"
}
die () { echo "$@" ; cleanup ; exit 1; }
NVM_ROOT="$(cd ../.. && pwd)"
TMP_DIR="$(mktemp -d)"
[ -n "${TMP_DIR}" ] || die 'could not create a temporary directory'
# a working directory laid out like an nvm directory, with a `default` alias and that version installed
mkdir -p "${TMP_DIR}/cwd/alias" "${TMP_DIR}/cwd/versions/node/v9.9.9/bin" || die 'could not create the working directory'
echo 'v9.9.9' > "${TMP_DIR}/cwd/alias/default" || die 'could not create the default alias'
make_echo "${TMP_DIR}/cwd/versions/node/v9.9.9/bin/node" 'v9.9.9'
# a HOME whose default install location is this checkout, and one with no install at all
mkdir "${TMP_DIR}/home" "${TMP_DIR}/empty-home" "${TMP_DIR}/xdg" || die 'could not create home directories'
ln -s "${NVM_ROOT}" "${TMP_DIR}/home/.nvm" || die 'could not link ~/.nvm'
ln -s "${NVM_ROOT}" "${TMP_DIR}/xdg/nvm" || die 'could not link the XDG nvm directory'
# sources nvm.sh from the working directory, with NVM_DIR unset, and prints the detected NVM_DIR and node
source_from_cwd() {
(
cd "${TMP_DIR}/cwd" || exit 1
unset NVM_DIR XDG_CONFIG_HOME
HOME="${1}"
export HOME
if [ -n "${2-}" ]; then
XDG_CONFIG_HOME="${2}"
export XDG_CONFIG_HOME
fi
set --
\. "${NVM_ROOT}/nvm.sh" >/dev/null 2>&1 || exit $?
echo "${NVM_DIR-}"
command -v node || :
)
}
try source_from_cwd "${TMP_DIR}/home"
[ "${CAPTURED_EXIT_CODE}" = 0 ] || die "sourcing nvm.sh without NVM_DIR failed with ${CAPTURED_EXIT_CODE}"
DETECTED="$(echo "${CAPTURED_STDOUT}" | sed -n 1p)"
NODE_PATH_FOUND="$(echo "${CAPTURED_STDOUT}" | sed -n 2p)"
case "${NODE_PATH_FOUND}" in
"${TMP_DIR}/cwd"/*) die "sourcing nvm.sh without NVM_DIR put the working directory's node on PATH: ${NODE_PATH_FOUND}" ;;
esac
case "${DETECTED}" in
"${NVM_ROOT}" | "${TMP_DIR}/home/.nvm") ;;
*) die "sourcing nvm.sh without NVM_DIR detected >${DETECTED}<; expected >${NVM_ROOT}< or >${TMP_DIR}/home/.nvm<" ;;
esac
# shells that cannot locate a sourced file use the default install location, or refuse when there is none
if [ "${DETECTED}" = "${TMP_DIR}/home/.nvm" ]; then
try source_from_cwd "${TMP_DIR}/empty-home" "${TMP_DIR}/xdg"
[ "${CAPTURED_EXIT_CODE}" = 0 ] || die "sourcing nvm.sh with XDG_CONFIG_HOME failed with ${CAPTURED_EXIT_CODE}"
DETECTED="$(echo "${CAPTURED_STDOUT}" | sed -n 1p)"
[ "${DETECTED}" = "${TMP_DIR}/xdg/nvm" ] || die "sourcing nvm.sh with XDG_CONFIG_HOME detected >${DETECTED}<; expected >${TMP_DIR}/xdg/nvm<"
try source_from_cwd "${TMP_DIR}/empty-home"
[ "${CAPTURED_EXIT_CODE}" != 0 ] || die "sourcing nvm.sh with no default install location succeeded, with NVM_DIR >${CAPTURED_STDOUT}<"
[ -z "${CAPTURED_STDOUT}" ] || die "sourcing nvm.sh with no default install location printed >${CAPTURED_STDOUT}<"
fi
cleanup