[Robustness] reject unsafe LTS alias names from a mirror's index.tab

A mirror-supplied LTS codename (field 10 of index.tab) was used verbatim as an alias filename,
so a hostile codename such as `../../../.bashrc` could make nvm_make_alias write outside $NVM_DIR/alias
- with the default layout, into shell startup files.
Constrain remote codenames to safe filename characters at ingestion in nvm_ls_remote_index_tab,
and reject any `..` path component in nvm_make_alias as a backstop for every caller.
This commit is contained in:
Jordan Harband
2026-07-15 13:46:52 -07:00
parent 882ed79ece
commit 9275c5badd
2 changed files with 48 additions and 0 deletions
+8
View File
@@ -1283,6 +1283,13 @@ nvm_make_alias() {
nvm_err "an alias target version is required" nvm_err "an alias target version is required"
return 2 return 2
fi fi
# slashes are legal (eg `lts/iron`), but a `..` component would escape the alias dir
case "/${ALIAS}/" in
*/../*)
nvm_err "invalid alias name: ${ALIAS}"
return 3
;;
esac
nvm_echo "${VERSION}" | tee "$(nvm_alias_path)/${ALIAS}" >/dev/null nvm_echo "${VERSION}" | tee "$(nvm_alias_path)/${ALIAS}" >/dev/null
} }
@@ -1797,6 +1804,7 @@ nvm_ls_remote_index_tab() {
command mkdir -p "$(nvm_alias_path)/lts" command mkdir -p "$(nvm_alias_path)/lts"
{ command awk '{ { command awk '{
if ($10 ~ /^\-?$/) { next } if ($10 ~ /^\-?$/) { next }
if (tolower($10) !~ /^[a-z0-9][a-z0-9._-]*$/) { next }
if ($10 && !a[tolower($10)]++) { if ($10 && !a[tolower($10)]++) {
if (alias) { print alias, version } if (alias) { print alias, version }
alias_name = "lts/" tolower($10) alias_name = "lts/" tolower($10)
+40
View File
@@ -0,0 +1,40 @@
#!/bin/sh
WORK="$PWD/ls_remote-traversal-work.$$"
export HOME="$WORK/home"
export NVM_DIR="$HOME/.nvm"
BASHRC="$HOME/.bashrc"
cleanup() {
unset -f die cleanup nvm_download
rm -rf "$WORK"
}
die () { echo "$@" ; cleanup ; exit 1; }
mkdir -p "$NVM_DIR/alias"
: nvm.sh
\. ../../../nvm.sh
# a malicious/compromised mirror: the LTS codename field ($10) carries path
# traversal, and the version field ($1) carries a command-substitution payload;
# a valid codename (Iron) is included to prove real aliases still get written
nvm_download() {
printf 'version\tdate\tfiles\tnpm\tv8\tuv\tzlib\topenssl\tmodules\tlts\tsecurity\n'
printf '$(>%s/pwned)\t2026-01-01\tlinux-x64\t-\t-\t-\t-\t-\t-\t../../../.bashrc\t-\n' "$WORK"
printf 'v20.0.0\t2026-01-01\tlinux-x64\t-\t-\t-\t-\t-\t-\tIron\t-\n'
}
nvm_ls_remote >/dev/null 2>&1 || true
[ ! -e "$BASHRC" ] || die "path traversal wrote outside the alias dir: $BASHRC was created"
[ ! -e "$WORK/pwned" ] || die "mirror-supplied payload landed: $WORK/pwned was created"
[ -f "$NVM_DIR/alias/lts/iron" ] || die "valid LTS alias lts/iron was not created"
nvm_make_alias 'lts/../../../escape' 'v1.0.0' 2>/dev/null && die 'nvm_make_alias accepted a traversing alias name'
[ ! -e "$NVM_DIR/../escape" ] || die 'nvm_make_alias wrote outside the alias dir'
nvm_make_alias 'lts/carbon' 'v8.0.0' >/dev/null 2>&1 || die 'nvm_make_alias rejected a valid alias name'
[ -f "$NVM_DIR/alias/lts/carbon" ] || die 'nvm_make_alias did not create a valid alias'
cleanup
echo "nvm_ls_remote LTS codename traversal: passed"