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:
@@ -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
18
nvm.sh
@@ -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 \
|
||||
|
||||
76
test/fast/Unit tests/mirror URL credentials are masked in output
Executable file
76
test/fast/Unit tests/mirror URL credentials are masked in output
Executable 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
|
||||
31
test/fast/Unit tests/nvm_sanitize_url
Executable file
31
test/fast/Unit tests/nvm_sanitize_url
Executable 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'
|
||||
Reference in New Issue
Block a user