Compare commits

...
2 Commits
Author SHA1 Message Date
Jordan Harband b17550a0b9 [Robustness] install.sh: nvm_download: avoid eval so arguments aren't re-parsed by the shell
Code scanning - action / CodeQL-Build (push) Has been cancelled
Tests: `nvm install-latest-npm` / matrix (push) Has been cancelled
Tests: linting / eclint (push) Has been cancelled
Tests: linting / dockerfile_lint (push) Has been cancelled
Tests: linting / doctoc (push) Has been cancelled
Tests: linting / test_naming (push) Has been cancelled
Tests: linting / check_exec (push) Has been cancelled
Tests: alpine / fast arm64 (alpine 3, bash) (push) Has been cancelled
Tests: alpine / fast arm64 (alpine 3, dash) (push) Has been cancelled
Tests: alpine / fast arm64 (alpine 3, sh) (push) Has been cancelled
Tests: alpine / fast arm64 (alpine 3, zsh) (push) Has been cancelled
Tests: alpine / fast x64 (alpine 3, bash) (push) Has been cancelled
Tests: alpine / fast x64 (alpine 3, dash) (push) Has been cancelled
Tests: alpine / fast x64 (alpine 3, sh) (push) Has been cancelled
Tests: alpine / fast x64 (alpine 3, zsh) (push) Has been cancelled
Tests: alpine / fast arm64 (alpine 3.15, bash) (push) Has been cancelled
Tests: alpine / fast arm64 (alpine 3.15, dash) (push) Has been cancelled
Tests: alpine / fast arm64 (alpine 3.15, sh) (push) Has been cancelled
Tests: alpine / fast arm64 (alpine 3.15, zsh) (push) Has been cancelled
Tests: alpine / fast x64 (alpine 3.15, bash) (push) Has been cancelled
Tests: alpine / fast x64 (alpine 3.15, dash) (push) Has been cancelled
Tests: alpine / fast x64 (alpine 3.15, sh) (push) Has been cancelled
Tests: alpine / fast x64 (alpine 3.15, zsh) (push) Has been cancelled
Tests: alpine / fast arm64 (alpine 3.19, bash) (push) Has been cancelled
Tests: alpine / fast arm64 (alpine 3.19, dash) (push) Has been cancelled
Tests: alpine / fast arm64 (alpine 3.19, sh) (push) Has been cancelled
Tests: alpine / fast x64 (alpine 3.19, bash) (push) Has been cancelled
Tests: alpine / fast x64 (alpine 3.19, dash) (push) Has been cancelled
Tests: alpine / fast x64 (alpine 3.19, sh) (push) Has been cancelled
Tests: alpine / fast x64 (alpine 3.19, zsh) (push) Has been cancelled
urchin tests / tests (bash, slow) (push) Has been cancelled
urchin tests / tests (bash, install_script) (push) Has been cancelled
urchin tests / tests (bash, installation_iojs) (push) Has been cancelled
Tests: release process / release (push) Has been cancelled
Tests: shellcheck / shellcheck_matrix (bash_completion, bash) (push) Has been cancelled
Tests: shellcheck / shellcheck_matrix (install.sh, bash) (push) Has been cancelled
Tests: shellcheck / shellcheck_matrix (nvm-exec, bash) (push) Has been cancelled
Tests: shellcheck / shellcheck_matrix (nvm.sh, bash) (push) Has been cancelled
Tests: shellcheck / shellcheck_matrix (nvm.sh, dash) (push) Has been cancelled
Tests: shellcheck / shellcheck_matrix (nvm.sh, ksh) (push) Has been cancelled
Tests: shellcheck / shellcheck_matrix (nvm.sh, sh) (push) Has been cancelled
Tests: xenial / xenial (bash) (push) Has been cancelled
Tests: xenial / xenial (dash) (push) Has been cancelled
Tests: xenial / xenial (sh) (push) Has been cancelled
Tests: xenial / xenial (zsh) (push) Has been cancelled
Tests: alpine / musl-binary arm64 (alpine 3, node v24.9.0) (push) Has been cancelled
Tests: alpine / musl-binary x64 (alpine 3, node v22.12.0) (push) Has been cancelled
Tests: alpine / musl-binary x64 (alpine 3.10, node v10.24.1) (push) Has been cancelled
Tests: alpine / musl-binary x64 (alpine 3.10, node v8.17.0) (push) Has been cancelled
Tests: alpine / musl-binary x64 (alpine 3.12, node v12.22.12) (push) Has been cancelled
Tests: alpine / musl-binary x64 (alpine 3.15, node v14.21.3) (push) Has been cancelled
Tests: alpine / musl-binary x64 (alpine 3.16, node v16.20.2) (push) Has been cancelled
Tests: alpine / musl-binary x64 (alpine 3.18, node v18.20.4) (push) Has been cancelled
Tests: alpine / musl-binary x64 (alpine 3.20, node v20.18.1) (push) Has been cancelled
Tests: fast / fast (bash, gawk) (push) Has been cancelled
Tests: fast / fast (dash, gawk) (push) Has been cancelled
Tests: installation_node / installation_node (zsh, without curl) (push) Has been cancelled
urchin tests / tests (bash, sourcing) (push) Has been cancelled
urchin tests / tests (dash, installation_iojs) (push) Has been cancelled
urchin tests / tests (dash, slow) (push) Has been cancelled
urchin tests / tests (dash, sourcing) (push) Has been cancelled
urchin tests / tests (sh, installation_iojs) (push) Has been cancelled
urchin tests / tests (sh, slow) (push) Has been cancelled
urchin tests / tests (sh, sourcing) (push) Has been cancelled
urchin tests / tests (zsh, installation_iojs) (push) Has been cancelled
urchin tests / tests (zsh, slow) (push) Has been cancelled
urchin tests / tests (zsh, sourcing) (push) Has been cancelled
update readme TOC / update readme TOC (push) Has been cancelled
Tests on Windows: `nvm install` / MSYS fail prefix nvm install (push) Has been cancelled
Tests on Windows: `nvm install` / MSYS nvm install (--default 12) (push) Has been cancelled
Tests on Windows: `nvm install` / MSYS nvm install (--lts) (push) Has been cancelled
Tests on Windows: `nvm install` / Cygwin nvm install (push) Has been cancelled
Tests: `nvm install-latest-npm` / nvm install-latest-npm (push) Has been cancelled
Tests: linting / all linting (push) Has been cancelled
Tests: xenial / all xenial tests (push) Has been cancelled
Tests on Windows: `nvm install` / tests, on windows (push) Has been cancelled
Tests: shellcheck / shellcheck (push) Has been cancelled
Tests: alpine / all alpine tests (push) Has been cancelled
Tests: fast / all fast tests (push) Has been cancelled
Tests: installation_iojs / all installation_iojs tests (push) Has been cancelled
Tests: fast / fast (zsh, gawk) (push) Has been cancelled
Tests: fast / fast (bash, mawk) (push) Has been cancelled
Tests: fast / fast (dash, mawk) (push) Has been cancelled
Tests: fast / fast (sh, mawk) (push) Has been cancelled
Tests: fast / fast (zsh, mawk) (push) Has been cancelled
Tests: installation_iojs / installation_iojs without curl (dash) (push) Has been cancelled
Tests: installation_iojs / installation_iojs without curl (sh) (push) Has been cancelled
Tests: installation_iojs / installation_iojs without curl (zsh) (push) Has been cancelled
Tests: installation_iojs / actually compile io.js from source (gcc 4.9 container) (push) Has been cancelled
Tests: installation_node / installation_node (bash) (push) Has been cancelled
Tests: installation_node / installation_node (bash, without curl) (push) Has been cancelled
Tests: installation_node / installation_node (dash) (push) Has been cancelled
Tests: installation_node / installation_node (dash, without curl) (push) Has been cancelled
Tests: installation_node / installation_node (sh) (push) Has been cancelled
Tests: installation_node / installation_node (sh, without curl) (push) Has been cancelled
Tests: installation_node / installation_node (zsh) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 10, Debian) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 10, Ubuntu-18.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 10, Ubuntu-20.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 12, Debian) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 12, Ubuntu-18.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 12, Ubuntu-20.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 14, Debian) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 14, Ubuntu-18.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 14, Ubuntu-20.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 16, Debian) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 16, Ubuntu-20.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 18, Debian) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 18, Ubuntu-20.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 21, Ubuntu-20.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 10, Debian) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 10, Ubuntu-18.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 10, Ubuntu-20.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 12, Debian) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 12, Ubuntu-18.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 12, Ubuntu-20.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 14, Debian) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 14, Ubuntu-18.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 14, Ubuntu-20.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 16, Debian) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 16, Ubuntu-18.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 16, Ubuntu-20.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 18, Debian) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 18, Ubuntu-20.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 21, Debian) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 21, Ubuntu-20.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, --lts, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 10, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 11, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 12, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 14, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 16, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 18, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 21, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, --lts, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 10, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 11, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 12, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 14, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 16, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 18, Alpine) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (script, 21, Alpine) (push) Has been cancelled
urchin tests / all test suites, all shells (push) Has been cancelled
Tests: alpine / fast arm64 (alpine 3.19, zsh) (push) Has been cancelled
Tests: alpine / musl-binary arm64 (alpine 3, node v22.21.1) (push) Has been cancelled
Tests: alpine / musl-binary arm64 (alpine 3.20, node v20.20.1) (push) Has been cancelled
Tests on Windows: `nvm install` / MSYS nvm install (--no-progress 10) (push) Has been cancelled
Tests: fast / fast (sh, gawk) (push) Has been cancelled
Tests: installation_iojs / installation_iojs without curl (bash) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 16, Ubuntu-18.04) (push) Has been cancelled
Tests on Windows: `nvm install` / WSL nvm install (, 21, Debian) (push) Has been cancelled
Tests: installation_node / all installation_node tests (push) Has been cancelled
`install.sh` has its own `nvm_download`,
which joined `"$@"` into a single string with `nvm_echo` and rewrote the curl-style flags with `sed`,
then ran the result through `eval command wget $ARGS`.
Flattening argv into a string is the root cause: every argument was re-parsed by the shell,
so any shell metacharacter in a legitimate value was interpreted rather than passed through.
An `$NVM_DIR` containing a space silently word-split and wrote to the wrong path,
an apostrophe or parentheses were a syntax error,
a glob character expanded against the cwd,
and an `$NVM_SOURCE` mirror URL with an `&` in its query string backgrounded `wget` and ran the remainder as a bogus command.
The `sed` rewrites were also unanchored substring replacements that would mangle a URL legitimately containing `-o `,
and `ARGS` was an undeclared global that `nvm_reset` never unset.

Translate the flags per-argument with a POSIX `set --` loop and invoke `command wget "$@"` instead,
so each argument stays a literal argv element.
This is the same shape `nvm.sh`'s `nvm_download` has used since 6d870d18,
and it stops the two implementations from diverging.

Unlike `nvm.sh`, no mirror-supplied data reaches this function:
its arguments come only from `$NVM_SOURCE`, `$NVM_INSTALL_GITHUB_REPO`, `$NVM_INSTALL_VERSION`, `$NVM_DIR`, `$XDG_CONFIG_HOME`, and `$HOME`, and `install.sh`'s `nvm_latest_version` is a hardcoded string rather than `nvm.sh`'s `nvm_get_latest`.
This is therefore a robustness fix and not a security one,
since the only party who can influence these arguments is the one already running the installer.
2026-07-15 15:55:21 -07:00
Jordan Harband 52047edfb1 [Tests] install_nvm_from_git: don't fail when master is at the latest release tag
The avoid_ref check read HEAD's tags via `git for-each-ref` with no --git-dir,
so it inspected the caller's checkout instead of the installed clone.
Whether it passed then depended on which ref triggered the workflow
(the same commit passed when built as the v0.40.6 tag but failed when built as master, since only the former had the tag present),
not on the install result.
Query the installed clone and compare commits,
so a HEAD that is legitimately the latest release commit
(eg master right after a release)
is exempted deterministically.
2026-07-15 15:02:22 -07:00
3 changed files with 133 additions and 14 deletions
+25 -11
View File
@@ -121,17 +121,31 @@ nvm_download() {
command curl --fail --compressed -q "$@"
elif nvm_has_executable "wget"; then
# Emulate curl with wget
ARGS=$(nvm_echo "$@" | command sed -e 's/--progress-bar /--progress=bar /' \
-e 's/--compressed //' \
-e 's/--fail //' \
-e 's/-L //' \
-e 's/-I /--server-response /' \
-e 's/-s /-q /' \
-e 's/-sS /-nv /' \
-e 's/-o /-O /' \
-e 's/-C - /-c /')
# shellcheck disable=SC2086
eval command wget $ARGS
local NVM_DOWNLOAD_WGET_COUNT
NVM_DOWNLOAD_WGET_COUNT=$#
local NVM_DOWNLOAD_WGET_SKIP
NVM_DOWNLOAD_WGET_SKIP=0
local NVM_DOWNLOAD_WGET_ARG
for NVM_DOWNLOAD_WGET_ARG in "$@"; do
if [ "${NVM_DOWNLOAD_WGET_SKIP}" = '1' ]; then
NVM_DOWNLOAD_WGET_SKIP=0
continue
fi
case "${NVM_DOWNLOAD_WGET_ARG}" in
'--progress-bar') set -- "$@" '--progress=bar' ;;
'--compressed') : ;;
'--fail') : ;;
'-L') : ;;
'-I') set -- "$@" '--server-response' ;;
'-s') set -- "$@" '-q' ;;
'-sS') set -- "$@" '-nv' ;;
'-o') set -- "$@" '-O' ;;
'-C') NVM_DOWNLOAD_WGET_SKIP=1; set -- "$@" '-c' ;;
*) set -- "$@" "${NVM_DOWNLOAD_WGET_ARG}" ;;
esac
done
shift "${NVM_DOWNLOAD_WGET_COUNT}"
command wget "$@"
fi
}
+9 -3
View File
@@ -63,9 +63,15 @@ test_install_data() {
echo "$current_ref" | grep -q "$ref" || die "install_nvm_from_git ${message} did not clone with ref ${ref}"
fi
local head_ref="$(git for-each-ref --points-at HEAD --format='%(refname:short)' 'refs/tags/')"
if [ -n "${avoid_ref}" ] && [ "${head_ref}" != "${avoid_ref}" ]; then
echo "${current_ref}" | grep -q "$avoid_ref" && die "install_nvm_from_git ${message} did clone with unwanted ref ${avoid_ref}"
if [ -n "${avoid_ref}" ] && echo "${current_ref}" | grep -q "${avoid_ref}"; then
# the avoided tag decorating HEAD is only legitimate if HEAD really is that
# tag's commit in the installed clone (eg master released at that tag); query
# the clone itself, not the caller's checkout, so the trigger ref cannot skew it
local avoid_changeset
avoid_changeset="$(git --git-dir "$NVM_DIR"/.git rev-parse --verify --quiet "refs/tags/${avoid_ref}^{commit}" 2>/dev/null)"
if [ "${avoid_changeset}" != "${current_changeset}" ]; then
die "install_nvm_from_git ${message} did clone with unwanted ref ${avoid_ref}"
fi
fi
if [ -n "$changeset" ]; then
+99
View File
@@ -0,0 +1,99 @@
#!/bin/sh
OLDPATH="$PATH"
WORK="$PWD/nvm_download-noeval-work.$$"
TEST_BIN="$WORK/bin"
ARGV_LOG="$WORK/argv.log"
PROOF="$WORK/nvm_injection_proof"
cleanup() {
unset -f die cleanup
rm -rf "$WORK"
export PATH="$OLDPATH"
}
die () { echo "$@" ; cleanup ; exit 1; }
NVM_ENV=testing \. ../../install.sh
mkdir -p "$TEST_BIN"
# fake curl/wget: record each received argument verbatim, then succeed
{
echo '#!/bin/sh'
echo ': > "$ARGV_LOG"'
echo 'for a in "$@"; do printf "%s\n" "$a" >> "$ARGV_LOG"; done'
echo 'exit 0'
} > "$TEST_BIN/curl"
chmod +x "$TEST_BIN/curl"
cp "$TEST_BIN/curl" "$TEST_BIN/wget"
# the wget-path calls below restrict PATH to $TEST_BIN so that no real curl is
# found; link sed and touch in so that a regression to the old sed|eval
# implementation fails this test by actually injecting, rather than by failing
# to find the binaries the injection needs
ln -s "$(command -v sed)" "$TEST_BIN/sed"
ln -s "$(command -v touch)" "$TEST_BIN/touch"
export ARGV_LOG
export PATH="$TEST_BIN:$OLDPATH"
URL="https://raw.githubusercontent.com/nvm-sh/nvm/HEAD/nvm.sh"
# given a url containing command-substitution syntax
INJECT_URL="http://example.test/v1\$(touch ${PROOF})/x"
# when nvm_download is invoked (curl path)
rm -f "$PROOF"
nvm_download "$INJECT_URL" -o - || die 'nvm_download (curl) returned nonzero on injection url'
# then the substitution must not have executed
[ ! -e "$PROOF" ] || die "command injection fired via curl path: proof file was created"
# and curl must have received the url as one literal argument
grep -Fxq "$INJECT_URL" "$ARGV_LOG" || die "curl did not receive the url as a single literal argument; got: $(cat "$ARGV_LOG")"
# given curl is unavailable (the wget-path calls run with PATH limited to our
# fake wget, so neither the fake nor the system curl is found)
rm -f "$TEST_BIN/curl"
# when nvm_download is invoked with the injection url (wget path)
rm -f "$PROOF"
( PATH="$TEST_BIN"; export PATH; nvm_download "$INJECT_URL" -o - ) || die 'nvm_download (wget) returned nonzero on injection url'
# then the substitution must not have executed
[ ! -e "$PROOF" ] || die "command injection fired via wget path: proof file was created"
grep -Fxq "$INJECT_URL" "$ARGV_LOG" || die "wget did not receive the url as a single literal argument; got: $(cat "$ARGV_LOG")"
# given an output path containing a space, as a real $NVM_DIR may
SPACED_DIR="$WORK/dir with space"
SPACED="$SPACED_DIR/nvm.sh"
mkdir -p "$SPACED_DIR"
# when nvm_download is invoked with it (wget path)
( PATH="$TEST_BIN"; export PATH; nvm_download -s "$URL" -o "$SPACED" ) || die 'nvm_download (wget) returned nonzero on spaced output path'
# then wget receives it as one argument rather than word-split
grep -Fxq "$SPACED" "$ARGV_LOG" || die "wget did not receive the spaced output path as a single argument; got: $(cat "$ARGV_LOG")"
# given a url with an ampersand in its query string, as a real mirror may
AMP_URL="http://example.test/nvm.sh?a=1&b=2"
# when nvm_download is invoked with it (wget path)
( PATH="$TEST_BIN"; export PATH; nvm_download -s "$AMP_URL" -o - ) || die 'nvm_download (wget) returned nonzero on ampersand url'
# then the url is passed intact rather than backgrounding the command
grep -Fxq "$AMP_URL" "$ARGV_LOG" || die "wget did not receive the ampersand url intact; got: $(cat "$ARGV_LOG")"
# when invoked with -L -C - --progress-bar URL -o FILE (wget path)
FILE="$WORK/target"
( PATH="$TEST_BIN"; export PATH; nvm_download -L -C - --progress-bar "$URL" -o "$FILE" ) || die 'nvm_download (wget) returned nonzero on normal url'
# then flags are translated to wget equivalents
grep -Fxqe "-c" "$ARGV_LOG" || die "wget did not translate -C - to -c; got: $(cat "$ARGV_LOG")"
grep -Fxqe "--progress=bar" "$ARGV_LOG" || die "wget did not translate --progress-bar; got: $(cat "$ARGV_LOG")"
grep -Fxqe "-O" "$ARGV_LOG" || die "wget did not translate -o to -O; got: $(cat "$ARGV_LOG")"
grep -Fxqe "-L" "$ARGV_LOG" && die "wget should drop -L; got: $(cat "$ARGV_LOG")"
grep -Fxqe "-C" "$ARGV_LOG" && die "wget should not pass -C through; got: $(cat "$ARGV_LOG")"
grep -Fxqe "-" "$ARGV_LOG" && die "wget should drop the lone - after -C; got: $(cat "$ARGV_LOG")"
# when invoked with -s (wget path)
( PATH="$TEST_BIN"; export PATH; nvm_download -s "$URL" -o "$FILE" ) || die 'nvm_download (wget) returned nonzero on -s url'
# then -s becomes -q
grep -Fxqe "-q" "$ARGV_LOG" || die "wget did not translate -s to -q; got: $(cat "$ARGV_LOG")"
cleanup
echo "nvm_download no eval injection: passed"