[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 committed 2026-10-23 08:53:59 -07:00
1 parent 37c578d973
commit 4a1f7d2875
4 files changed
+121 -6

No files matched your search

@@ -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
+31
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'