From d8b39c746e110b7d8d5c7666bc4da020661ce38b Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 18 Oct 2026 23:03:46 -0700 Subject: [PATCH] [Fix] `nvm_sanitize_auth_header`: strip newlines, so wrapped `base64` credentials work `sed` works one line at a time, so the allowlist never removed newlines inside `NVM_AUTH_HEADER`. A `Basic` credential built with GNU `base64`, which wraps its output at 76 columns, therefore reached the downloader split across several lines, and authentication failed (wget rejects such a header outright). Filter with `tr -cd` instead, using the same set of allowed characters, so CR and LF are deleted like any other disallowed character and a wrapped credential is joined back into one line Discovered by Anthropic's OSS Scanner, as bug ANT-2026-W1E9TAK3; the scanner correctly classified it as not a vulnerability. --- nvm.sh | 6 ++++-- test/fast/Unit tests/nvm_sanitize_auth_header | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/nvm.sh b/nvm.sh index 6b1d6eaf..f8d6f0dd 100755 --- a/nvm.sh +++ b/nvm.sh @@ -217,8 +217,10 @@ nvm_sanitize_auth_header() { # `Basic ` and `Bearer ` survive intact. # '~' is also allowed, completing RFC 7235 `token68` (and thus RFC 6750 # `b64token`), so opaque Bearer tokens containing it are not corrupted. - # Note: '-' must be at the end of the bracket expression to be treated as a literal. - nvm_echo "$1" | command sed 's/[^a-zA-Z0-9 :_.+/=~-]//g' + # Note: '-' must be at the end of the set to be treated as a literal. + # `tr`, not `sed`: sed is line-based and would keep newlines, eg from a + # `base64` credential that was wrapped at 76 columns. + nvm_echo "$1" | command tr -cd 'a-zA-Z0-9 :_.+/=~-' } nvm_has_system_node() { diff --git a/test/fast/Unit tests/nvm_sanitize_auth_header b/test/fast/Unit tests/nvm_sanitize_auth_header index d7ea8192..731310e5 100755 --- a/test/fast/Unit tests/nvm_sanitize_auth_header +++ b/test/fast/Unit tests/nvm_sanitize_auth_header @@ -49,4 +49,13 @@ B64TOKEN="Bearer mF_9.B5f-4.1JqM~+/=" RESULT=$(nvm_sanitize_auth_header "${B64TOKEN}") [ "${RESULT}" = "${B64TOKEN}" ] || die "FAIL: b64token chars were stripped. Got: '${RESULT}'" +# Test 8: a `Basic` credential that `base64` wrapped at 76 columns is rejoined into one line +WRAPPED="Basic $(printf 'YS1yYXRoZXItbG9uZy11c2VybmFtZUBleGFtcGxlLmNvbTphbi1ldmVuLWxvbmdlci1wYXNzd29y\nZCE=')" +RESULT=$(nvm_sanitize_auth_header "${WRAPPED}") +[ "${RESULT}" = "Basic YS1yYXRoZXItbG9uZy11c2VybmFtZUBleGFtcGxlLmNvbTphbi1ldmVuLWxvbmdlci1wYXNzd29yZCE=" ] || die "FAIL: wrapped base64 credential was not rejoined. Got: '${RESULT}'" + +# Test 9: CR, LF, and blank lines are all removed +RESULT=$(nvm_sanitize_auth_header "$(printf 'Bearer a\r\nb\n\nc')") +[ "${RESULT}" = "Bearer abc" ] || die "FAIL: line breaks survived sanitization. Got: '${RESULT}'" + echo "All nvm_sanitize_auth_header tests passed"