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

[Fix] nvm_download_artifact, nvm debug: mask credentials in mirror URLs when printing them

`nvm_get_mirror` accepts `user:password@` in `$NVM_NODEJS_ORG_MIRROR` and `$NVM_IOJS_ORG_MIRROR`,
but `nvm_download_artifact` printed the full tarball URL in its "Downloading",
"download from ... failed",
and "HTTP 404 at URL" messages,
and `nvm debug` printed both mirror variables as-is,
so the credentials showed up in terminal output and build logs.

Add `nvm_sanitize_url`,
which replaces the userinfo part of any URL with `***@`,
and use it in those messages.
The download itself still uses the full URL,
so authenticating to the mirror keeps working.
The README now notes that `$NVM_AUTH_HEADER` is the recommended way to authenticate to a mirror

Discovered by Anthropic's OSS Scanner, as ANT-2026-X03SZ7C6; the scanner reported it as a vulnerability, but it is not one.
This commit is contained in:
Jordan Harband
2026-10-18 23:13:19 -07:00
parent 37c578d973
commit 4a1f7d2875
4 changed files with 121 additions and 6 deletions

View File

@@ -667,6 +667,8 @@ To pass an Authorization header through to the mirror url, set `$NVM_AUTH_HEADER
NVM_AUTH_HEADER="Bearer secret-token" nvm install node
```
`$NVM_AUTH_HEADER` is the recommended way to authenticate to a mirror. Credentials embedded in the mirror URL itself (`https://user:password@host/...`) also work, and nvm prints such URLs with the credentials masked as `***@`.
### Platforms without official binaries
nodejs.org does not publish a binary for every platform and architecture nvm can detect. Where it does not, `nvm install` falls back by default to compiling from source, which is slow and needs a C++ toolchain (see [Important Notes](#important-notes)); set `$NVM_NO_SOURCE_FALLBACK` to `1` to make a missing binary an error instead.

18
nvm.sh
View File

@@ -3168,17 +3168,17 @@ nvm_download_artifact() {
nvm_err "Removing the broken local cache..."
command rm -rf "${TARBALL}"
fi
nvm_err "Downloading ${TARBALL_URL}..."
nvm_err "Downloading $(nvm_sanitize_url "${TARBALL_URL}")..."
# stdout is reserved for the tarball path, so nothing the downloader prints may reach it
nvm_download -L -C - "${PROGRESS_BAR}" "${TARBALL_URL}" -o "${TARBALL}" >&2 || {
command rm -rf "${TARBALL}" "${tmpdir}"
nvm_err "download from ${TARBALL_URL} failed"
nvm_err "download from $(nvm_sanitize_url "${TARBALL_URL}") failed"
return 4
}
if nvm_grep '404 Not Found' "${TARBALL}" >/dev/null; then
command rm -rf "${TARBALL}" "${tmpdir}"
nvm_err "HTTP 404 at URL ${TARBALL_URL}"
nvm_err "HTTP 404 at URL $(nvm_sanitize_url "${TARBALL_URL}")"
return 5
fi
@@ -3697,6 +3697,12 @@ nvm_sanitize_path() {
nvm_echo "${SANITIZED_PATH}"
}
# Replace the `user:password@` part of any URL with `***@`, for printing.
# Like curl and wget, everything up to the last `@` before the path is userinfo.
nvm_sanitize_url() {
nvm_echo "${1-}" | command sed -e 's#://[^/[:space:]]*@#://***@#g'
}
nvm_is_natural_num() {
if [ -z "$1" ]; then
return 4
@@ -4093,8 +4099,8 @@ nvm() {
nvm_err "\${PATH}: $(nvm_sanitize_path "${PATH}")"
nvm_err "\$PREFIX: '$(nvm_sanitize_path "${PREFIX-}")'"
nvm_err "\${NPM_CONFIG_PREFIX}: '$(nvm_sanitize_path "${NPM_CONFIG_PREFIX-}")'"
nvm_err "\$NVM_NODEJS_ORG_MIRROR: '${NVM_NODEJS_ORG_MIRROR-}'"
nvm_err "\$NVM_IOJS_ORG_MIRROR: '${NVM_IOJS_ORG_MIRROR-}'"
nvm_err "\$NVM_NODEJS_ORG_MIRROR: '$(nvm_sanitize_url "${NVM_NODEJS_ORG_MIRROR-}")'"
nvm_err "\$NVM_IOJS_ORG_MIRROR: '$(nvm_sanitize_url "${NVM_IOJS_ORG_MIRROR-}")'"
nvm_err "shell version: '$(${SHELL} --version | command head -n 1)'"
nvm_err "uname -a: '$(command uname -a | command awk '{$2=""; print}' | command xargs)'"
nvm_err "checksum binary: '$(nvm_get_checksum_binary 2>/dev/null)'"
@@ -5480,7 +5486,7 @@ nvm() {
nvm_install_lock_name nvm_acquire_install_lock nvm_release_install_lock \
nvm_list_aliases nvm_make_alias nvm_print_alias_file nvm_print_alias_path \
nvm_print_default_alias nvm_print_formatted_alias nvm_resolve_local_alias \
nvm_replace_literal nvm_sanitize_path nvm_has_colors nvm_has_italics nvm_process_parameters \
nvm_replace_literal nvm_sanitize_path nvm_sanitize_url nvm_has_colors nvm_has_italics nvm_process_parameters \
nvm_node_version_has_solaris_binary nvm_iojs_version_has_solaris_binary \
nvm_curl_libz_support nvm_command_info nvm_is_zsh nvm_stdout_is_terminal \
nvm_npmrc_bad_news_bears nvm_sanitize_auth_header \

View File

@@ -0,0 +1,76 @@
#!/bin/sh
WORK="$PWD/mirror-url-credentials-work.$$"
ORIG_NVM_DIR="${NVM_DIR-}"
cleanup() {
unset -f nvm_download nvm_get_checksum nvm_compare_checksum
unset NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR
NVM_DIR="${ORIG_NVM_DIR}"
rm -rf "${WORK}"
}
die () { echo "$@" ; cleanup ; exit 1; }
# args: description, output
assert_masked() {
case "${2}" in
*s3cr3t* | *ci-bot*) die "${1} printed the mirror URL credentials; got >${2}<" ;;
esac
case "${2}" in
*'://***@mirror.example.com/dist'*) ;;
*) die "${1} did not print the masked mirror URL; got >${2}<" ;;
esac
}
\. ../../../nvm.sh
mkdir -p "${WORK}"
NVM_NODEJS_ORG_MIRROR='https://ci-bot:s3cr3t@mirror.example.com/dist'
NVM_IOJS_ORG_MIRROR='https://ci-bot:s3cr3t@mirror.example.com/dist'
export NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR
assert_masked 'nvm debug' "$(nvm debug 2>&1)"
NVM_DIR="${WORK}"
nvm_get_checksum() { nvm_echo 'fake_checksum'; }
nvm_compare_checksum() { return 0; }
# nvm_download still receives the unmasked URL
nvm_download() {
case "$*" in
*"${NVM_NODEJS_ORG_MIRROR}/v20.0.0/"*) ;;
*) nvm_err 'nvm_download did not receive the mirror URL credentials' ;;
esac
return "${DOWNLOAD_EXIT_CODE-0}"
}
OUTPUT="$(nvm_download_artifact node source std v20.0.0 2>&1)"
assert_masked 'nvm_download_artifact' "${OUTPUT}"
case "${OUTPUT}" in
*'did not receive'*) die "${OUTPUT}" ;;
esac
OUTPUT="$(DOWNLOAD_EXIT_CODE=1 nvm_download_artifact node source std v20.0.0 2>&1)"
case "${OUTPUT}" in
*'download from '*' failed'*) ;;
*) die "expected a download failure message; got >${OUTPUT}<" ;;
esac
assert_masked 'nvm_download_artifact, on a failed download,' "${OUTPUT#*...}"
nvm_download() {
while [ "$#" -gt 0 ]; do
if [ "$1" = '-o' ]; then
nvm_echo '404 Not Found' > "$2"
return 0
fi
shift
done
}
OUTPUT="$(nvm_download_artifact node source std v20.0.0 2>&1)"
case "${OUTPUT}" in
*'HTTP 404 at URL '*) ;;
*) die "expected an HTTP 404 message; got >${OUTPUT}<" ;;
esac
assert_masked 'nvm_download_artifact, on a 404,' "${OUTPUT#*...}"
cleanup

View File

@@ -0,0 +1,31 @@
#!/bin/sh
die () { echo "$@" ; exit 1; }
\. ../../../nvm.sh
check() {
ACTUAL="$(nvm_sanitize_url "${1}")"
[ "${ACTUAL}" = "${2}" ] || die "nvm_sanitize_url '${1}': expected >${2}<, got >${ACTUAL}<"
}
# URLs without userinfo are unchanged
check '' ''
check 'https://nodejs.org/dist' 'https://nodejs.org/dist'
check 'https://nodejs.org' 'https://nodejs.org'
check 'http://127.0.0.1:8080/dist/v20.0.0/node-v20.0.0.tar.xz' 'http://127.0.0.1:8080/dist/v20.0.0/node-v20.0.0.tar.xz'
check 'https://[::1]:8080/dist' 'https://[::1]:8080/dist'
check 'https://nodejs.org/a@b/dist' 'https://nodejs.org/a@b/dist'
# userinfo is masked
check 'https://user:pw@nexus.example.com/repository/nodejs-dist' 'https://***@nexus.example.com/repository/nodejs-dist'
check 'https://user:pw@nexus.example.com' 'https://***@nexus.example.com'
check 'https://token@nexus.example.com:8443/dist' 'https://***@nexus.example.com:8443/dist'
check 'http://user:p%40w@[::1]:8080/dist' 'http://***@[::1]:8080/dist'
# everything up to the last `@` before the path is userinfo; an `@` in the path is kept
check 'https://user:p@w@nexus.example.com/a@b' 'https://***@nexus.example.com/a@b'
# every URL in multiline text is masked
check 'URL:http://user:pw@a.example.com/x [1/1]
https://user:pw@b.example.com/y' 'URL:http://***@a.example.com/x [1/1]
https://***@b.example.com/y'