From 10a277ec47447101d7fc60e86fefafb4bc87d83d Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 2 Sep 2026 19:40:47 -0700 Subject: [PATCH] [Fix] `nvm_get_mirror`: allow ports, userinfo, IPv6 hosts, and percent-encoding 3c480159 correctly made this validator actually enforce, instead of the no-op `awk '{ $0 ~ "..." }'` it had been, but its character class had never been audited against real mirror URLs: it has no `:`, so enabling enforcement silently rejected every mirror URL bearing a port. $ NVM_NODEJS_ORG_MIRROR=https://mirror.internal:8443/dist nvm_ls_remote $NVM_NODEJS_ORG_MIRROR and $NVM_IOJS_ORG_MIRROR may only contain a URL No request is made at all, so an internal mirror on a non-default port has been unusable since v0.40.5. Allow RFC 3986 `unreserved` plus the authority and path punctuation a mirror URL legitimately needs: `:` for a port, `@` for userinfo, `[`/`]` for an IPv6 literal host, and `%` for percent-encoding. `sub-delims` and the query/fragment delimiters stay rejected: nvm appends a path to this value, so a query string could never have worked, and several of those characters are shell metacharacters. --- nvm.sh | 9 ++++++++- test/fast/Unit tests/nvm_get_mirror | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/nvm.sh b/nvm.sh index 0c48cb94..cc18543d 100755 --- a/nvm.sh +++ b/nvm.sh @@ -2388,7 +2388,14 @@ nvm_get_mirror() { esac - if ! nvm_echo "${NVM_MIRROR}" | command awk '{ if ($0 !~ /^https?:\/\/[a-zA-Z0-9.\/_-]+$/) exit 1 }'; then + # Allow RFC 3986 `unreserved` (A-Za-z0-9-._~) plus the authority and path + # punctuation a mirror URL legitimately needs: `:` for a port, `@` for + # userinfo, `[`/`]` for an IPv6 literal host, `%` for percent-encoding. + # `sub-delims` (!$&'()*+,;=) and the query/fragment delimiters (?#) stay out: + # nvm appends a path to this value, so a query string could never work anyway, + # and several of those characters are shell metacharacters. + # Note: in a bracket expression `]` must come first and `-` last to be literal. + if ! nvm_echo "${NVM_MIRROR}" | command awk '{ if ($0 !~ /^https?:\/\/[]a-zA-Z0-9._~:\/@%[-]+$/) exit 1 }'; then nvm_err '$NVM_NODEJS_ORG_MIRROR and $NVM_IOJS_ORG_MIRROR may only contain a URL' return 2 fi diff --git a/test/fast/Unit tests/nvm_get_mirror b/test/fast/Unit tests/nvm_get_mirror index 0c16ff4d..fd2ea940 100755 --- a/test/fast/Unit tests/nvm_get_mirror +++ b/test/fast/Unit tests/nvm_get_mirror @@ -52,3 +52,26 @@ testMirrors 'not a url' testMirrors 'ftp://wrong-scheme' testMirrors 'http://' testMirrors 'javascript:alert(1)' + +# a query string can never work, since nvm appends a path to the mirror +testMirrors 'https://nodejs.org/dist?token=abc' +testMirrors 'https://nodejs.org/dist#frag' + +testAcceptedMirrors() { + NVM_NODEJS_ORG_MIRROR="${1-}" + [ "$(nvm_get_mirror node std)" = "${1-}" ] || die "NVM_NODEJS_ORG_MIRROR should accept ${1-}" + unset NVM_NODEJS_ORG_MIRROR + + NVM_IOJS_ORG_MIRROR="${1-}" + [ "$(nvm_get_mirror iojs std)" = "${1-}" ] || die "NVM_IOJS_ORG_MIRROR should accept ${1-}" + unset NVM_IOJS_ORG_MIRROR +} + +# a private mirror on a non-default port is a URL, and must not be rejected +testAcceptedMirrors 'https://mirror.internal:8443/dist' +testAcceptedMirrors 'http://localhost:8080/dist' +# an IPv6 literal host, userinfo, percent-encoding, and `~` are all legal too +testAcceptedMirrors 'http://[::1]:8080/dist' +testAcceptedMirrors 'https://user:pw@nexus.corp.example.com/repository/nodejs-dist' +testAcceptedMirrors 'https://mirror.internal/a%20b/dist' +testAcceptedMirrors 'https://mirror.internal/~dist'