1
0
mirror of https://github.com/nvm-sh/nvm.git synced 2026-10-25 16:03:56 +08:00

[Fix] bash_completion: list alias and version names without expanding them

`__nvm_generate_completion` passed its word list to `compgen -W`,
which runs every word through shell expansion.
Since that list includes alias file names and installed version directory names,
pressing Tab after `nvm use`, `nvm alias`, etc,
ran any command substitution in those names,
expanded variables in them, and globbed `*` against the current directory.
Now the list is split with globbing disabled,
and each word is matched against the current word as a literal prefix,
so names are offered exactly as written

Discovered by Anthropic's OSS Scanner, as bug ANT-2026-QDARMGPO; the scanner correctly classified it as not a vulnerability.
This commit is contained in:
Jordan Harband
2026-10-18 23:04:41 -07:00
parent d8b39c746e
commit 6b9650c47f
2 changed files with 131 additions and 2 deletions

View File

@@ -8,9 +8,25 @@ fi
__nvm_generate_completion() {
declare current_word
declare word
declare glob_was_off
current_word="${COMP_WORDS[COMP_CWORD]}"
# shellcheck disable=SC2207
COMPREPLY=($(compgen -W "$1" -- "${current_word}"))
# not `compgen -W`: it expands every word (including command substitution),
# and the words include alias and version directory names
COMPREPLY=()
glob_was_off=false
case $- in
*f*) glob_was_off=true ;;
esac
set -f
for word in $1; do
case "${word}" in
"${current_word}"*) COMPREPLY+=("${word}") ;;
esac
done
if [ "${glob_was_off}" = false ]; then
set +f
fi
return 0
}

View File

@@ -0,0 +1,113 @@
#!/bin/sh
die () { echo "$@" ; cleanup ; exit 1; }
cleanup() {
rm -rf "${TEST_DIR-}"
unset TEST_DIR NVM_SH BASH_COMPLETION OUTPUT EXPECTED_OUTPUT SUBCOMMAND COMPLETION_SHELL
}
\. ../../common.sh
NVM_SH="$(cd ../../.. && pwd)/nvm.sh"
BASH_COMPLETION="$(cd ../../.. && pwd)/bash_completion"
# an isolated NVM_DIR whose alias and version directory names `compgen -W` would
# expand, and a cwd holding only one file, which a glob would list and next to
# which any command run by such an expansion would leave another file
TEST_DIR="${PWD}/bash_completion_expansion_tmp"
mkdir -p "${TEST_DIR}/nvm/alias/lts" "${TEST_DIR}/nvm/versions/node/v0.0.1/bin" "${TEST_DIR}/cwd" || die 'failed to create test dirs'
make_echo "${TEST_DIR}/nvm/versions/node/v0.0.1/bin/node" 'v0.0.1' || die 'failed to create test node binary'
mkdir "${TEST_DIR}/nvm/versions/node/v\$(:>expanded-version)" || die 'failed to create test version dir'
echo v0.0.1 > "${TEST_DIR}/nvm/alias/my_alias"
echo v0.0.1 > "${TEST_DIR}/nvm/alias/lts/iron"
echo v0.0.1 > "${TEST_DIR}/nvm/alias/\$(:>expanded-dollar)"
echo v0.0.1 > "${TEST_DIR}/nvm/alias/\`:>expanded-backtick\`"
echo v0.0.1 > "${TEST_DIR}/nvm/alias/\${NVM_DIR}"
echo v0.0.1 > "${TEST_DIR}/nvm/alias/*"
: > "${TEST_DIR}/cwd/globbed-file"
# prints the sorted completions for the given command line, the last word being completed
complete_nvm() {
(
cd "${TEST_DIR}/cwd" || exit 1
case "${COMPLETION_SHELL}" in
bash)
BASH_ENV='' NVM_DIR="${TEST_DIR}/nvm" bash --noprofile --norc -c '
\. "${1}" --no-use
\. "${2}"
shift 2
COMP_WORDS=("${@}")
COMP_CWORD=$(($# - 1))
__nvm
printf "%s\n" "${COMPREPLY[@]}"
' bash "${NVM_SH}" "${BASH_COMPLETION}" "$@"
;;
zsh)
# bashcompinit runs `complete -F` functions from its `compgen`, under `emulate -L sh`
NVM_DIR="${TEST_DIR}/nvm" zsh -f -c '
\. "${1}" --no-use
\. "${2}"
shift 2
COMP_WORDS=("${@}")
COMP_CWORD=$(($# - 1))
compgen -F __nvm -- "${@[-1]}"
' zsh "${NVM_SH}" "${BASH_COMPLETION}" "$@"
;;
esac
) | LC_ALL=C sort
}
for COMPLETION_SHELL in bash zsh; do
command -v "${COMPLETION_SHELL}" > /dev/null 2>&1 || continue
for SUBCOMMAND in use run exec ls list uninstall alias unalias; do
OUTPUT="$(complete_nvm nvm "${SUBCOMMAND}" '')"
[ "$(ls -A "${TEST_DIR}/cwd")" = 'globbed-file' ] || die "${COMPLETION_SHELL}: completing 'nvm ${SUBCOMMAND}' ran a command from an alias or version name: $(ls -A "${TEST_DIR}/cwd")"
case "${OUTPUT}" in
*"${TEST_DIR}"*) die "${COMPLETION_SHELL}: completing 'nvm ${SUBCOMMAND}' expanded a variable from an alias name: ${OUTPUT}" ;;
*globbed-file*) die "${COMPLETION_SHELL}: completing 'nvm ${SUBCOMMAND}' globbed an alias name: ${OUTPUT}" ;;
esac
done
OUTPUT="$(complete_nvm nvm alias foo '')"
[ "$(ls -A "${TEST_DIR}/cwd")" = 'globbed-file' ] || die "${COMPLETION_SHELL}: completing 'nvm alias foo' ran a command from an alias or version name: $(ls -A "${TEST_DIR}/cwd")"
OUTPUT="$(complete_nvm nvm unalias '')"
EXPECTED_OUTPUT='$(:>expanded-dollar)
${NVM_DIR}
*
`:>expanded-backtick`
iojs
lts/iron
my_alias
node
stable
unstable'
[ "${OUTPUT}" = "${EXPECTED_OUTPUT}" ] || die "${COMPLETION_SHELL}: completing 'nvm unalias' should list alias names literally: expected >${EXPECTED_OUTPUT}<, got >${OUTPUT}<"
OUTPUT="$(complete_nvm nvm use '$(')"
EXPECTED_OUTPUT='$(:>expanded-dollar)'
[ "${OUTPUT}" = "${EXPECTED_OUTPUT}" ] || die "${COMPLETION_SHELL}: completing 'nvm use \$(' should give >${EXPECTED_OUTPUT}<, got >${OUTPUT}<"
OUTPUT="$(complete_nvm nvm use 'v')"
EXPECTED_OUTPUT='v$(:>expanded-version)
v0.0.1'
[ "${OUTPUT}" = "${EXPECTED_OUTPUT}" ] || die "${COMPLETION_SHELL}: completing 'nvm use v' should list version names literally: expected >${EXPECTED_OUTPUT}<, got >${OUTPUT}<"
OUTPUT="$(complete_nvm nvm use m)"
EXPECTED_OUTPUT='my_alias'
[ "${OUTPUT}" = "${EXPECTED_OUTPUT}" ] || die "${COMPLETION_SHELL}: completing 'nvm use m' should give >${EXPECTED_OUTPUT}<, got >${OUTPUT}<"
OUTPUT="$(complete_nvm nvm use lts/)"
EXPECTED_OUTPUT='lts/iron'
[ "${OUTPUT}" = "${EXPECTED_OUTPUT}" ] || die "${COMPLETION_SHELL}: completing 'nvm use lts/' should give >${EXPECTED_OUTPUT}<, got >${OUTPUT}<"
OUTPUT="$(complete_nvm nvm un)"
EXPECTED_OUTPUT='unalias
uninstall
unload'
[ "${OUTPUT}" = "${EXPECTED_OUTPUT}" ] || die "${COMPLETION_SHELL}: completing 'nvm un' should give >${EXPECTED_OUTPUT}<, got >${OUTPUT}<"
done
cleanup