mirror of
https://github.com/nvm-sh/nvm.git
synced 2026-10-27 09:13:43 +08:00
[Fix] nvm_download: only send $NVM_AUTH_HEADER to URLs under a mirror it applies to
The header was added to every download, whatever the host. With only `$NVM_NODEJS_ORG_MIRROR` set, a private mirror's credential therefore also went to the default `https://iojs.org/dist` (eg, from `nvm ls-remote`), and with only `$NVM_IOJS_ORG_MIRROR` set, to `https://nodejs.org/dist`; it also went to `npmjs.org` when installing an old npm. Add `nvm_auth_header_applies_to`, which uses `nvm_get_mirror` and accepts a URL only when it is under a mirror, compared literally, with a `/` after the mirror's prefix. When exactly one of the two mirror variables is set, the header belongs to that mirror alone; when both or neither are set, both mirrors (including the defaults, when neither is) still receive it, so the documented example keeps working. An invalid mirror matches nothing, and no empty `Authorization:` header is sent when sanitizing leaves nothing Discovered by Anthropic's OSS Scanner, as ANT-2026-YPW87EDH; 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
|
||||
```
|
||||
|
||||
The header is only sent with requests for URLs under a mirror, never to any other host. If exactly one of `$NVM_NODEJS_ORG_MIRROR` and `$NVM_IOJS_ORG_MIRROR` is set, only that mirror receives it; if both are set, both do; if neither is, the default mirrors (`https://nodejs.org/dist` and `https://iojs.org/dist`) do.
|
||||
|
||||
`$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
|
||||
|
||||
53
nvm.sh
53
nvm.sh
@@ -156,7 +156,24 @@ nvm_download() {
|
||||
local sanitized_header
|
||||
sanitized_header=''
|
||||
if [ -n "${NVM_AUTH_HEADER:-}" ]; then
|
||||
sanitized_header="$(nvm_sanitize_auth_header "${NVM_AUTH_HEADER}")"
|
||||
local NVM_DOWNLOAD_AUTH
|
||||
NVM_DOWNLOAD_AUTH=''
|
||||
local NVM_DOWNLOAD_ARG
|
||||
for NVM_DOWNLOAD_ARG in "$@"; do
|
||||
case "${NVM_DOWNLOAD_ARG}" in
|
||||
http://* | https://*)
|
||||
if nvm_auth_header_applies_to "${NVM_DOWNLOAD_ARG}"; then
|
||||
NVM_DOWNLOAD_AUTH=1
|
||||
else
|
||||
NVM_DOWNLOAD_AUTH=''
|
||||
break
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [ -n "${NVM_DOWNLOAD_AUTH}" ]; then
|
||||
sanitized_header="$(nvm_sanitize_auth_header "${NVM_AUTH_HEADER}")"
|
||||
fi
|
||||
fi
|
||||
|
||||
local NVM_DOWNLOADER
|
||||
@@ -202,13 +219,43 @@ nvm_download() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "${NVM_AUTH_HEADER:-}" ]; then
|
||||
if [ -n "${sanitized_header}" ]; then
|
||||
set -- "$@" --header "Authorization: ${sanitized_header}"
|
||||
fi
|
||||
|
||||
command "${NVM_DOWNLOADER}" "$@"
|
||||
}
|
||||
|
||||
# args: url
|
||||
# Whether `$NVM_AUTH_HEADER` applies to a request for the given URL: only URLs
|
||||
# under a mirror qualify, never any other host (eg, npmjs.org). When exactly one
|
||||
# of `$NVM_NODEJS_ORG_MIRROR` and `$NVM_IOJS_ORG_MIRROR` is set, the header is
|
||||
# that mirror's credential, so the other flavor's default host does not get it.
|
||||
nvm_auth_header_applies_to() {
|
||||
local NVM_AUTH_FLAVOR
|
||||
local NVM_AUTH_MIRROR
|
||||
for NVM_AUTH_FLAVOR in node iojs; do
|
||||
case "${NVM_AUTH_FLAVOR}" in
|
||||
node)
|
||||
if [ -z "${NVM_NODEJS_ORG_MIRROR-}" ] && [ -n "${NVM_IOJS_ORG_MIRROR-}" ]; then
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
iojs)
|
||||
if [ -z "${NVM_IOJS_ORG_MIRROR-}" ] && [ -n "${NVM_NODEJS_ORG_MIRROR-}" ]; then
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
# an invalid mirror matches nothing; the download itself reports it
|
||||
NVM_AUTH_MIRROR="$(nvm_get_mirror "${NVM_AUTH_FLAVOR}" std 2>/dev/null)" || continue
|
||||
case "${1-}" in
|
||||
"${NVM_AUTH_MIRROR}"/*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
nvm_sanitize_auth_header() {
|
||||
# Remove potentially dangerous characters; allow the full base64 (A-Za-z0-9+/=)
|
||||
# and base64url (A-Za-z0-9-_=) charsets, plus the space, colon, dot, and
|
||||
@@ -5530,7 +5577,7 @@ nvm() {
|
||||
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 \
|
||||
nvm_npmrc_bad_news_bears nvm_sanitize_auth_header nvm_auth_header_applies_to \
|
||||
nvm_get_colors nvm_set_colors nvm_print_color_code nvm_wrap_with_color_code \
|
||||
nvm_print_color_legend \
|
||||
nvm_echo_with_colors nvm_err_with_colors \
|
||||
|
||||
128
test/fast/Unit tests/nvm_auth_header_applies_to
Executable file
128
test/fast/Unit tests/nvm_auth_header_applies_to
Executable file
@@ -0,0 +1,128 @@
|
||||
#!/bin/sh
|
||||
|
||||
die () { echo "$@" ; exit 1; }
|
||||
|
||||
\. ../../../nvm.sh
|
||||
|
||||
unset NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR
|
||||
|
||||
# args: url; asserts it applies, with exit code 0 and no output
|
||||
applies() {
|
||||
OUTPUT="$(nvm_auth_header_applies_to "${1}" 2>&1)"
|
||||
EXIT_CODE=$?
|
||||
[ "${EXIT_CODE}" = 0 ] || die "nvm_auth_header_applies_to '${1}' (node mirror >${NVM_NODEJS_ORG_MIRROR-(unset)}<, iojs mirror >${NVM_IOJS_ORG_MIRROR-(unset)}<): expected exit code 0, got ${EXIT_CODE}"
|
||||
[ -z "${OUTPUT}" ] || die "nvm_auth_header_applies_to '${1}': expected no output, got >${OUTPUT}<"
|
||||
}
|
||||
|
||||
# args: url; asserts it does not apply, with exit code 1 and no output
|
||||
does_not_apply() {
|
||||
OUTPUT="$(nvm_auth_header_applies_to "${1}" 2>&1)"
|
||||
EXIT_CODE=$?
|
||||
[ "${EXIT_CODE}" = 1 ] || die "nvm_auth_header_applies_to '${1}' (node mirror >${NVM_NODEJS_ORG_MIRROR-(unset)}<, iojs mirror >${NVM_IOJS_ORG_MIRROR-(unset)}<): expected exit code 1, got ${EXIT_CODE}"
|
||||
[ -z "${OUTPUT}" ] || die "nvm_auth_header_applies_to '${1}': expected no output, got >${OUTPUT}<"
|
||||
}
|
||||
|
||||
# no argument, or an empty one, never applies
|
||||
OUTPUT="$(nvm_auth_header_applies_to 2>&1)"
|
||||
EXIT_CODE=$?
|
||||
[ "${EXIT_CODE}" = 1 ] || die "no argument: expected exit code 1, got ${EXIT_CODE}"
|
||||
[ -z "${OUTPUT}" ] || die "no argument: expected no output, got >${OUTPUT}<"
|
||||
does_not_apply ''
|
||||
|
||||
# neither mirror set: both defaults apply, nothing else does
|
||||
applies 'https://nodejs.org/dist/index.tab'
|
||||
applies 'https://nodejs.org/dist/v22.0.0/node-v22.0.0-linux-x64.tar.xz'
|
||||
applies 'https://iojs.org/dist/index.tab'
|
||||
does_not_apply 'https://nodejs.org/dist'
|
||||
does_not_apply 'https://nodejs.org/distx/index.tab'
|
||||
does_not_apply 'https://nodejs.org.example.com/dist/index.tab'
|
||||
does_not_apply 'https://nodejs.org/download/index.tab'
|
||||
does_not_apply 'http://nodejs.org/dist/index.tab'
|
||||
does_not_apply 'HTTPS://NODEJS.ORG/dist/index.tab'
|
||||
does_not_apply 'https://npmjs.org/install.sh'
|
||||
does_not_apply 'https://registry.npmjs.org/npm/-/npm-1.4.28.tgz'
|
||||
does_not_apply 'x https://nodejs.org/dist/index.tab'
|
||||
|
||||
# an empty mirror variable counts as unset
|
||||
NVM_NODEJS_ORG_MIRROR=''
|
||||
NVM_IOJS_ORG_MIRROR=''
|
||||
applies 'https://nodejs.org/dist/index.tab'
|
||||
applies 'https://iojs.org/dist/index.tab'
|
||||
|
||||
# only the node mirror set (the iojs one empty or unset): only it applies
|
||||
NVM_NODEJS_ORG_MIRROR='https://mirror.example.com/node'
|
||||
applies 'https://mirror.example.com/node/index.tab'
|
||||
does_not_apply 'https://iojs.org/dist/index.tab'
|
||||
does_not_apply 'https://nodejs.org/dist/index.tab'
|
||||
does_not_apply 'https://mirror.example.com/nodex/index.tab'
|
||||
does_not_apply 'https://mirror.example.com/iojs/index.tab'
|
||||
unset NVM_IOJS_ORG_MIRROR
|
||||
applies 'https://mirror.example.com/node/index.tab'
|
||||
does_not_apply 'https://iojs.org/dist/index.tab'
|
||||
|
||||
# only the iojs mirror set (the node one empty or unset): only it applies
|
||||
NVM_NODEJS_ORG_MIRROR=''
|
||||
NVM_IOJS_ORG_MIRROR='https://mirror.example.com/iojs'
|
||||
applies 'https://mirror.example.com/iojs/index.tab'
|
||||
does_not_apply 'https://nodejs.org/dist/index.tab'
|
||||
does_not_apply 'https://iojs.org/dist/index.tab'
|
||||
unset NVM_NODEJS_ORG_MIRROR
|
||||
applies 'https://mirror.example.com/iojs/index.tab'
|
||||
does_not_apply 'https://nodejs.org/dist/index.tab'
|
||||
|
||||
# both set: each applies, the defaults do not
|
||||
NVM_NODEJS_ORG_MIRROR='https://mirror.example.com/node'
|
||||
NVM_IOJS_ORG_MIRROR='http://127.0.0.1:8080/iojs'
|
||||
applies 'https://mirror.example.com/node/index.tab'
|
||||
applies 'http://127.0.0.1:8080/iojs/index.tab'
|
||||
does_not_apply 'https://nodejs.org/dist/index.tab'
|
||||
does_not_apply 'https://iojs.org/dist/index.tab'
|
||||
does_not_apply 'https://127.0.0.1:8080/iojs/index.tab'
|
||||
|
||||
# both set, one invalid: the invalid one matches nothing (silently), the other still applies
|
||||
NVM_NODEJS_ORG_MIRROR='ftp://mirror.example.com/node'
|
||||
applies 'http://127.0.0.1:8080/iojs/index.tab'
|
||||
does_not_apply 'ftp://mirror.example.com/node/index.tab'
|
||||
does_not_apply 'https://nodejs.org/dist/index.tab'
|
||||
|
||||
# only an invalid mirror set: nothing applies, not even the defaults
|
||||
unset NVM_IOJS_ORG_MIRROR
|
||||
for INVALID in 'ftp://mirror.example.com/node' 'https://mirror.example.com/a b' 'https://mirror.example.com/$(id)' 'https://mirror.example.com/?q=1' 'mirror.example.com/node'; do
|
||||
NVM_NODEJS_ORG_MIRROR="${INVALID}"
|
||||
does_not_apply "${INVALID}/index.tab"
|
||||
does_not_apply 'https://nodejs.org/dist/index.tab'
|
||||
does_not_apply 'https://iojs.org/dist/index.tab'
|
||||
# an invalid mirror must not act as an empty prefix
|
||||
does_not_apply '/index.tab'
|
||||
does_not_apply '/'
|
||||
done
|
||||
|
||||
# a mirror with no path: only its own host applies, not a host it prefixes
|
||||
NVM_NODEJS_ORG_MIRROR='https://mirror.example.com'
|
||||
applies 'https://mirror.example.com/v22.0.0/SHASUMS256.txt'
|
||||
does_not_apply 'https://mirror.example.com.evil.example/v22.0.0/SHASUMS256.txt'
|
||||
does_not_apply 'https://mirror.example.com:8443/v22.0.0/SHASUMS256.txt'
|
||||
does_not_apply 'https://mirror.example.com@evil.example/v22.0.0/SHASUMS256.txt'
|
||||
|
||||
# a mirror with userinfo and a port is compared as written
|
||||
NVM_NODEJS_ORG_MIRROR='https://user:pw@mirror.example.com:8443/node'
|
||||
applies 'https://user:pw@mirror.example.com:8443/node/index.tab'
|
||||
does_not_apply 'https://mirror.example.com:8443/node/index.tab'
|
||||
|
||||
# a mirror with a trailing slash applies to the URLs nvm builds from it
|
||||
NVM_NODEJS_ORG_MIRROR='https://mirror.example.com/node/'
|
||||
applies 'https://mirror.example.com/node//index.tab'
|
||||
does_not_apply 'https://mirror.example.com/node/index.tab'
|
||||
|
||||
# glob characters in a mirror match only literally
|
||||
NVM_NODEJS_ORG_MIRROR='http://[::1]:8080/[n]ode'
|
||||
applies 'http://[::1]:8080/[n]ode/index.tab'
|
||||
does_not_apply 'http://[::1]:8080/node/index.tab'
|
||||
does_not_apply 'http://1:8080/[n]ode/index.tab'
|
||||
|
||||
# the mirror variables are left as they were, and no helper variables leak
|
||||
[ "${NVM_NODEJS_ORG_MIRROR}" = 'http://[::1]:8080/[n]ode' ] || die "expected NVM_NODEJS_ORG_MIRROR to be unchanged, got >${NVM_NODEJS_ORG_MIRROR}<"
|
||||
[ -z "${NVM_IOJS_ORG_MIRROR+set}" ] || die "expected NVM_IOJS_ORG_MIRROR to stay unset, got >${NVM_IOJS_ORG_MIRROR}<"
|
||||
nvm_auth_header_applies_to 'http://[::1]:8080/[n]ode/index.tab' || die 'expected the glob mirror to apply when called in the current shell'
|
||||
[ -z "${NVM_AUTH_FLAVOR+set}" ] || die "expected NVM_AUTH_FLAVOR not to leak, got >${NVM_AUTH_FLAVOR}<"
|
||||
[ -z "${NVM_AUTH_MIRROR+set}" ] || die "expected NVM_AUTH_MIRROR not to leak, got >${NVM_AUTH_MIRROR}<"
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/bin/sh
|
||||
|
||||
WORK="$PWD/nvm_download-auth-mirror-work.$$"
|
||||
TEST_BIN="$WORK/bin"
|
||||
REQUEST_LOG="$WORK/request.log"
|
||||
|
||||
cleanup() {
|
||||
unset -f die cleanup sends_header
|
||||
unset NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR
|
||||
rm -rf "$WORK"
|
||||
export PATH="$OLDPATH"
|
||||
}
|
||||
die () { echo "$@" ; cleanup ; exit 1; }
|
||||
|
||||
\. ../../../nvm.sh
|
||||
|
||||
\. ../../common.sh
|
||||
|
||||
OLDPATH="$PATH"
|
||||
|
||||
# fake curl: record every argument, and anything given on stdin, then succeed
|
||||
make_fake_curl "$TEST_BIN" '{ for a in "$@"; do printf "%s\n" "$a"; done; cat; } > "$REQUEST_LOG"'
|
||||
VERSION_MESSAGE="curl 7.99.0 (fake)"
|
||||
export REQUEST_LOG
|
||||
export PATH="$TEST_BIN:$OLDPATH"
|
||||
|
||||
# args: url; whether nvm_download handed the credential to curl for that url
|
||||
sends_header() {
|
||||
NVM_AUTH_HEADER='Bearer test-token' nvm_download -L -s "$1" -o - < /dev/null || die "nvm_download returned nonzero for $1"
|
||||
grep -Fq 'test-token' "$REQUEST_LOG"
|
||||
}
|
||||
|
||||
# given no mirror is configured
|
||||
unset NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR
|
||||
# then both default mirrors are sent the header, and nothing else is
|
||||
sends_header 'https://nodejs.org/dist/index.tab' || die 'no mirror: header not sent to the default node mirror'
|
||||
sends_header 'https://iojs.org/dist/index.tab' || die 'no mirror: header not sent to the default iojs mirror'
|
||||
! sends_header 'https://npmjs.org/install.sh' || die 'no mirror: header sent to npmjs.org'
|
||||
! sends_header 'https://nodejs.org.example.com/dist/index.tab' || die 'no mirror: header sent to a host that merely starts with the mirror host'
|
||||
! sends_header 'https://nodejs.org/distx/index.tab' || die 'no mirror: header sent to a path that merely starts with the mirror path'
|
||||
|
||||
# given only the node mirror is configured
|
||||
NVM_NODEJS_ORG_MIRROR='http://127.0.0.1:8080/dist'
|
||||
export NVM_NODEJS_ORG_MIRROR
|
||||
# then only that mirror is sent the header
|
||||
sends_header 'http://127.0.0.1:8080/dist/index.tab' || die 'node mirror: header not sent to it'
|
||||
sends_header 'http://127.0.0.1:8080/dist/v22.0.0/SHASUMS256.txt' || die 'node mirror: header not sent to a path under it'
|
||||
! sends_header 'https://iojs.org/dist/index.tab' || die 'node mirror: header sent to the default iojs mirror'
|
||||
! sends_header 'https://nodejs.org/dist/index.tab' || die 'node mirror: header sent to the default node mirror'
|
||||
! sends_header 'http://127.0.0.1:8080/distx/index.tab' || die 'node mirror: header sent outside the mirror path'
|
||||
! sends_header 'https://npmjs.org/install.sh' || die 'node mirror: header sent to npmjs.org'
|
||||
|
||||
# given only the iojs mirror is configured
|
||||
unset NVM_NODEJS_ORG_MIRROR
|
||||
NVM_IOJS_ORG_MIRROR='http://127.0.0.1:8080/iojs'
|
||||
export NVM_IOJS_ORG_MIRROR
|
||||
# then only that mirror is sent the header
|
||||
sends_header 'http://127.0.0.1:8080/iojs/index.tab' || die 'iojs mirror: header not sent to it'
|
||||
! sends_header 'https://nodejs.org/dist/index.tab' || die 'iojs mirror: header sent to the default node mirror'
|
||||
! sends_header 'https://iojs.org/dist/index.tab' || die 'iojs mirror: header sent to the default iojs mirror'
|
||||
|
||||
# given both mirrors are configured
|
||||
NVM_NODEJS_ORG_MIRROR='http://127.0.0.1:8080/dist'
|
||||
export NVM_NODEJS_ORG_MIRROR
|
||||
# then both are sent the header, and the default hosts are not
|
||||
sends_header 'http://127.0.0.1:8080/dist/index.tab' || die 'both mirrors: header not sent to the node mirror'
|
||||
sends_header 'http://127.0.0.1:8080/iojs/index.tab' || die 'both mirrors: header not sent to the iojs mirror'
|
||||
! sends_header 'https://nodejs.org/dist/index.tab' || die 'both mirrors: header sent to the default node mirror'
|
||||
! sends_header 'https://iojs.org/dist/index.tab' || die 'both mirrors: header sent to the default iojs mirror'
|
||||
|
||||
# given an invalid mirror, nothing is sent the header (rather than everything)
|
||||
NVM_NODEJS_ORG_MIRROR='ftp://127.0.0.1/dist'
|
||||
unset NVM_IOJS_ORG_MIRROR
|
||||
! sends_header 'https://nodejs.org/dist/index.tab' || die 'invalid mirror: header sent to the default node mirror'
|
||||
! sends_header 'https://iojs.org/dist/index.tab' || die 'invalid mirror: header sent to the default iojs mirror'
|
||||
|
||||
# given a mirror whose name has glob characters, they match only literally
|
||||
NVM_NODEJS_ORG_MIRROR='http://127.0.0.1:8080/[d]ist'
|
||||
! sends_header 'http://127.0.0.1:8080/dist/index.tab' || die 'glob mirror: header sent to a URL matching the mirror as a pattern'
|
||||
sends_header 'http://127.0.0.1:8080/[d]ist/index.tab' || die 'glob mirror: header not sent to the mirror itself'
|
||||
|
||||
cleanup
|
||||
Reference in New Issue
Block a user