From 67268ebcb6e1a26ae3d71b3710d6333bb7358662 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 2 Nov 2026 11:34:39 -0600 Subject: [PATCH] [Fix] `nvm_process_nvmrc`: don't show the content of a symlinked `.nvmrc` `.nvmrc` is found and read through symlinks, so a repository that commits `.nvmrc` as a symlink to a path outside itself (eg, `../../.npmrc`) made a plain `nvm use`, `nvm install`, or `nvm which` (or `nvm-exec`, or the README's auto-switching shell hook) print that file: the invalid `.nvmrc` message shows everything that was parsed, even with `--silent`, and a one-line file was taken as the version, which is printed in the "Found" message and in later errors. A symlinked `.nvmrc` is still followed, so linking to another version file (eg, `.node-version`, or a parent directory's `.nvmrc`) keeps working, but its content is never shown, and it is valid only if it names a version or an existing alias, via a new `nvm_is_version_or_alias` helper. --- nvm.sh | 36 +++++++++- ...does not show a symlinked .nvmrc's content | 71 +++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) create mode 100755 test/fast/Unit tests/nvm_process_nvmrc does not show a symlinked .nvmrc's content diff --git a/nvm.sh b/nvm.sh index aee928e4..e72717c9 100755 --- a/nvm.sh +++ b/nvm.sh @@ -942,11 +942,43 @@ EOF nvm_echo "${unpaired_line}" } +nvm_is_version_or_alias() { + case "${1-}" in + system | 'lts/*') return 0 ;; + lts/-*) + case "${1#lts/-}" in + '' | *[!0-9]*) return 1 ;; + esac + return 0 + ;; + lts/*) + case "${1#lts/}" in + '' | *[!a-z]*) return 1 ;; + esac + return 0 + ;; + esac + nvm_is_valid_version "${1-}" || nvm_alias "${1-}" > /dev/null 2>&1 +} + nvm_process_nvmrc() { local NVMRC_PATH NVMRC_PATH="$1" - nvm_process_nvmrc_content "$(command cat "${NVMRC_PATH}")" + if [ ! -L "${NVMRC_PATH}" ]; then + nvm_process_nvmrc_content "$(command cat "${NVMRC_PATH}")" + return + fi + + # a symlink can point at any file, so never print its content, + # and only accept a version or an existing alias, which would be printed later + local NVMRC_VERSION + if ! NVMRC_VERSION="$(nvm_process_nvmrc_content "$(command cat "${NVMRC_PATH}")" 2>/dev/null)" \ + || ! nvm_is_version_or_alias "${NVMRC_VERSION}"; then + nvm_err "invalid .nvmrc! '${NVMRC_PATH}' is a symbolic link, so it must contain a version or an alias name; its content is not shown." + return 1 + fi + nvm_echo "${NVMRC_VERSION}" } nvm_rc_version() { @@ -6533,7 +6565,7 @@ nvm() { nvm_print_color_legend \ nvm_echo_with_colors nvm_err_with_colors \ nvm_get_artifact_compression nvm_install_binary_extract nvm_extract_tarball \ - nvm_process_nvmrc nvm_process_nvmrc_content nvm_nvmrc_invalid_msg \ + nvm_process_nvmrc nvm_process_nvmrc_content nvm_nvmrc_invalid_msg nvm_is_version_or_alias \ nvm_write_nvmrc nvm_hash_reset \ >/dev/null 2>&1 unset NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR NVM_DIR \ diff --git a/test/fast/Unit tests/nvm_process_nvmrc does not show a symlinked .nvmrc's content b/test/fast/Unit tests/nvm_process_nvmrc does not show a symlinked .nvmrc's content new file mode 100755 index 00000000..88a8a0a8 --- /dev/null +++ b/test/fast/Unit tests/nvm_process_nvmrc does not show a symlinked .nvmrc's content @@ -0,0 +1,71 @@ +#!/bin/sh + +die () { echo "$@" ; cleanup ; exit 1; } + +cleanup() { + rm -rf "${TEST_DIR-}" + nvm unalias nvmrc_symlink_alias > /dev/null 2>&1 + unset TEST_DIR +} + +: nvm.sh +\. ../../../nvm.sh + +\. ../../common.sh + +TEST_DIR="${PWD}/nvm_process_nvmrc_symlink_tmp" +mkdir -p "${TEST_DIR}/home" "${TEST_DIR}/project/packages/a" || die 'failed to create test dirs' +printf '//registry.npmjs.org/:_authToken=SECRET\n' > "${TEST_DIR}/home/.npmrc" +printf 'https://user:SECRET@github.com\n' > "${TEST_DIR}/home/.git-credentials" + +NVMRC="${TEST_DIR}/project/.nvmrc" + +for TARGET in .npmrc .git-credentials; do + rm -f "${NVMRC}" + ln -s "../home/${TARGET}" "${NVMRC}" || die 'failed to create the symlink' + + try_err nvm_process_nvmrc "${NVMRC}" + [ "${CAPTURED_EXIT_CODE}" != 0 ] || die "expected a symlink to ${TARGET} to be invalid; got exit code ${CAPTURED_EXIT_CODE}" + case "${CAPTURED_STDERR}" in + *SECRET*) die "expected a symlink to ${TARGET} to not show its content; got \`${CAPTURED_STDERR}\`" ;; + esac + [ "${CAPTURED_STDERR}" = "invalid .nvmrc! '${NVMRC}' is a symbolic link, so it must contain a version or an alias name; its content is not shown." ] || die "expected the symlink error for ${TARGET}; got \`${CAPTURED_STDERR}\`" + + try nvm_process_nvmrc "${NVMRC}" + [ -z "${CAPTURED_STDOUT}" ] || die "expected no version from a symlink to ${TARGET}; got \`${CAPTURED_STDOUT}\`" +done + +nvm alias nvmrc_symlink_alias v0.0.1 > /dev/null 2>&1 || die 'failed to create an alias' + +# a deliberate symlink to another version file, in or above the project, still works +for VERSION in v20.1.0 20 'v20.0.0-rc.1' node system 'lts/*' lts/iron lts/-1 nvmrc_symlink_alias; do + rm -f "${NVMRC}" "${TEST_DIR}/project/packages/a/.nvmrc" + printf '%s # comment\n' "${VERSION}" > "${NVMRC}" + ln -s ../../.nvmrc "${TEST_DIR}/project/packages/a/.nvmrc" || die 'failed to create the symlink' + + try nvm_process_nvmrc "${TEST_DIR}/project/packages/a/.nvmrc" + [ "${CAPTURED_EXIT_CODE}" = 0 ] || die "expected a symlink to an .nvmrc with \`${VERSION}\` to be valid; got exit code ${CAPTURED_EXIT_CODE}" + [ "${CAPTURED_STDOUT}" = "${VERSION}" ] || die "expected \`${VERSION}\` from a symlinked .nvmrc; got \`${CAPTURED_STDOUT}\`" +done + +for VERSION in not_an_alias lts/Iron 'lts/' 'lts/-x'; do + rm -f "${TEST_DIR}/project/.node-version" + printf '%s\n' "${VERSION}" > "${TEST_DIR}/project/.node-version" + rm -f "${NVMRC}" + ln -s .node-version "${NVMRC}" || die 'failed to create the symlink' + + try_err nvm_process_nvmrc "${NVMRC}" + [ "${CAPTURED_EXIT_CODE}" != 0 ] || die "expected a symlink to a file with \`${VERSION}\` to be invalid; got exit code ${CAPTURED_EXIT_CODE}" + case "${CAPTURED_STDERR}" in + *"${VERSION}"*) die "expected a symlink to a file with \`${VERSION}\` to not show its content; got \`${CAPTURED_STDERR}\`" ;; + esac +done + +# a regular file still shows what was parsed +rm -f "${NVMRC}" +printf 'foo=bar\n' > "${NVMRC}" +try_err nvm_process_nvmrc "${NVMRC}" +[ "${CAPTURED_EXIT_CODE}" != 0 ] || die "expected an .nvmrc without a version to be invalid; got exit code ${CAPTURED_EXIT_CODE}" +nvm_echo "${CAPTURED_STDERR}" | strip_colors | nvm_grep -q '^foo=bar$' || die "expected a regular .nvmrc's parsed content to be shown; got \`${CAPTURED_STDERR}\`" + +cleanup