[Fix] nvm_download: avoid eval so mirror-supplied version strings can't inject commands

`nvm_download` built a curl/wget command string and ran it with `eval`.
The download URLs embed the version string taken from the mirror's `index.tab`,
which is untrusted.
Wrapping each argument in double quotes inside the `eval` does not prevent command substitution,
so a version field such as `v1$(touch /tmp/proof)` was executed by the shell.
This bypassed the earlier quoting hardening in 0ce8f5a.

Pass every argument as a literal argv element instead of constructing a string for `eval`,
on both the curl and wget paths,
so URL arguments are never re-parsed by the shell.
The wget flag translation is now done per-argument with a POSIX
`set --` loop rather than `sed` over the joined string.
The auth header is sanitized and added once,
before invoking the downloader.
This commit is contained in:
Jordan Harband
2026-06-02 17:41:44 -07:00
parent d264b796a3
commit 6d870d182c
2 changed files with 140 additions and 39 deletions

93
nvm.sh
View File

@@ -115,47 +115,62 @@ nvm_get_latest() {
nvm_echo "${NVM_LATEST_URL##*/}"
}
# Every argument is passed through as a literal argv element so that untrusted,
# mirror-supplied version strings in the URLs are never re-parsed by the shell
# (which would allow command substitution / OS command injection).
nvm_download() {
if nvm_has "curl"; then
local CURL_COMPRESSED_FLAG=""
local CURL_HEADER_FLAG=""
local sanitized_header
if [ -n "${NVM_AUTH_HEADER:-}" ]; then
sanitized_header=$(nvm_sanitize_auth_header "${NVM_AUTH_HEADER}")
CURL_HEADER_FLAG="--header \"Authorization: ${sanitized_header}\""
fi
if nvm_curl_use_compression; then
CURL_COMPRESSED_FLAG="--compressed"
fi
local NVM_DOWNLOAD_ARGS
NVM_DOWNLOAD_ARGS=''
for arg in "$@"; do
NVM_DOWNLOAD_ARGS="${NVM_DOWNLOAD_ARGS} \"$arg\""
done
eval "curl -q --fail ${CURL_COMPRESSED_FLAG:-} ${CURL_HEADER_FLAG:-} ${NVM_DOWNLOAD_ARGS}"
elif nvm_has "wget"; then
# Emulate curl with wget
ARGS=$(nvm_echo "$@" | command sed "
s/--progress-bar /--progress=bar /
s/--compressed //
s/--fail //
s/-L //
s/-I /--server-response /
s/-s /-q /
s/-sS /-nv /
s/-o /-O /
s/-C - /-c /
")
if [ -n "${NVM_AUTH_HEADER:-}" ]; then
sanitized_header=$(nvm_sanitize_auth_header "${NVM_AUTH_HEADER}")
ARGS="${ARGS} --header \"Authorization: ${sanitized_header}\""
fi
# shellcheck disable=SC2086
eval wget $ARGS
local sanitized_header
sanitized_header=''
if [ -n "${NVM_AUTH_HEADER:-}" ]; then
sanitized_header="$(nvm_sanitize_auth_header "${NVM_AUTH_HEADER}")"
fi
local NVM_DOWNLOADER
NVM_DOWNLOADER=''
if nvm_has "curl"; then
NVM_DOWNLOADER='curl'
set -- -q --fail "$@"
if nvm_curl_use_compression; then
set -- --compressed "$@"
fi
elif nvm_has "wget"; then
NVM_DOWNLOADER='wget'
# Emulate curl with wget
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}"
fi
if [ -z "${NVM_DOWNLOADER}" ]; then
return 0
fi
if [ -n "${NVM_AUTH_HEADER:-}" ]; then
set -- "$@" --header "Authorization: ${sanitized_header}"
fi
"${NVM_DOWNLOADER}" "$@"
}
nvm_sanitize_auth_header() {