From 3f8411e58d40b904e4ff6a96512391f56bd95bf7 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 2 Nov 2026 11:27:17 -0600 Subject: [PATCH] [Fix] `nvm_write_nvmrc`: don't write through a symlinked `.nvmrc` `nvm use --save` and `nvm install --save` wrote the version to `$PWD/.nvmrc` with `tee`, which follows a symlink, so a repository that commits `.nvmrc` as a symlink to a path outside itself made `--save` truncate any file the user can write (eg, a shell startup file or `~/.ssh/authorized_keys`) and replace it with the version string, or create the target of a dangling link. The link itself was left in place, so `git status` stayed clean. `nvm_write_nvmrc` now refuses, with a warning, when `.nvmrc` in the current directory is a symlink, dangling or not. --- nvm.sh | 7 +++ ...ite_nvmrc does not write through a symlink | 48 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100755 test/fast/Unit tests/nvm_write_nvmrc does not write through a symlink diff --git a/nvm.sh b/nvm.sh index e4576577..aee928e4 100755 --- a/nvm.sh +++ b/nvm.sh @@ -4376,6 +4376,13 @@ nvm_write_nvmrc() { if [ "${VERSION_STRING}" = '∞' ] || [ "${VERSION_STRING}" = 'N/A' ]; then return 1 fi + # writing through a symlink would overwrite its target, which can be any file + if [ -L "$PWD"/.nvmrc ]; then + if [ "${NVM_SILENT:-0}" -ne 1 ]; then + nvm_err "Warning: Not writing version number ($VERSION_STRING) to .nvmrc, because it is a symbolic link" + fi + return 3 + fi echo "${VERSION_STRING}" | tee "$PWD"/.nvmrc > /dev/null || { if [ "${NVM_SILENT:-0}" -ne 1 ]; then nvm_err "Warning: Unable to write version number ($VERSION_STRING) to .nvmrc" diff --git a/test/fast/Unit tests/nvm_write_nvmrc does not write through a symlink b/test/fast/Unit tests/nvm_write_nvmrc does not write through a symlink new file mode 100755 index 00000000..5ea0a39a --- /dev/null +++ b/test/fast/Unit tests/nvm_write_nvmrc does not write through a symlink @@ -0,0 +1,48 @@ +#!/bin/sh + +die () { echo "$@" ; cleanup ; exit 1; } + +cleanup() { + cd "${ORIG_PWD}" || : + nvm deactivate > /dev/null 2>&1 + rm -rf "${TEST_DIR-}" "${NVM_DIR}/${TEST_VERSION}" + unset TEST_DIR TEST_VERSION ORIG_PWD NVM_SILENT +} + +: nvm.sh +\. ../../../nvm.sh + +\. ../../common.sh + +ORIG_PWD="${PWD}" +TEST_VERSION='v0.2.4' +make_fake_node "${TEST_VERSION}" || die 'failed to create a fake node' + +TEST_DIR="${PWD}/nvm_write_nvmrc_symlink_tmp" +mkdir -p "${TEST_DIR}/project" || die 'failed to create test dirs' +echo 'KEEPME' > "${TEST_DIR}/target" +ln -s ../target "${TEST_DIR}/project/.nvmrc" || die 'failed to create the symlink' + +cd "${TEST_DIR}/project" || die 'failed to enter the project dir' + +try_err nvm_write_nvmrc "${TEST_VERSION}" +[ "${CAPTURED_EXIT_CODE}" = 3 ] || die "expected exit code 3 for a symlinked .nvmrc; got ${CAPTURED_EXIT_CODE}" +[ "${CAPTURED_STDERR}" = "Warning: Not writing version number (${TEST_VERSION}) to .nvmrc, because it is a symbolic link" ] || die "expected a warning about the symlink; got \`${CAPTURED_STDERR}\`" +[ "$(cat ../target)" = 'KEEPME' ] || die "expected the symlink's target to be unchanged; got \`$(cat ../target)\`" +[ -L .nvmrc ] || die 'expected .nvmrc to still be a symlink' + +nvm use --save "${TEST_VERSION}" > /dev/null 2>&1 +[ "$(cat ../target)" = 'KEEPME' ] || die "expected \`nvm use --save\` to leave the symlink's target unchanged; got \`$(cat ../target)\`" + +rm .nvmrc +ln -s ../dangling .nvmrc || die 'failed to create the dangling symlink' +try_err nvm_write_nvmrc "${TEST_VERSION}" +[ "${CAPTURED_EXIT_CODE}" = 3 ] || die "expected exit code 3 for a dangling symlinked .nvmrc; got ${CAPTURED_EXIT_CODE}" +[ ! -e ../dangling ] || die "expected a dangling symlink's target to not be created" + +export NVM_SILENT=1 +try_err nvm_write_nvmrc "${TEST_VERSION}" +[ -z "${CAPTURED_STDERR}" ] || die "expected NVM_SILENT=1 to silence the warning; got \`${CAPTURED_STDERR}\`" +unset NVM_SILENT + +cleanup