diff --git a/nvm.sh b/nvm.sh index f70b6b4e..05e147cc 100755 --- a/nvm.sh +++ b/nvm.sh @@ -800,6 +800,12 @@ nvm_alias_path() { nvm_version_path() { local VERSION VERSION="${1-}" + case "/${VERSION}/" in + */../*) + nvm_err "invalid version: ${VERSION}" + return 3 + ;; + esac if [ -z "${VERSION}" ]; then nvm_err 'version is required' return 3 @@ -1498,6 +1504,15 @@ nvm_alias() { return 2 fi + # slashes are legal (eg `lts/iron`), but a `..` component would read outside + # the alias dir; `nvm_make_alias` rejects the same shape on the write side + case "/${ALIAS}/" in + */../*) + nvm_err "invalid alias name: ${ALIAS}" + return 3 + ;; + esac + local NVM_ALIAS_PATH NVM_ALIAS_PATH="$(nvm_alias_path)/${ALIAS}" if [ ! -f "${NVM_ALIAS_PATH}" ]; then diff --git a/test/fast/Unit tests/nvm_alias path traversal b/test/fast/Unit tests/nvm_alias path traversal new file mode 100755 index 00000000..1dff7c4d --- /dev/null +++ b/test/fast/Unit tests/nvm_alias path traversal @@ -0,0 +1,67 @@ +#!/bin/sh + +ORIG_NVM_DIR="${NVM_DIR}" +TMP_DIR='' + +cleanup () { + case "${TMP_DIR}" in + /*) rm -rf "${TMP_DIR}" ;; + esac + NVM_DIR="${ORIG_NVM_DIR}" + export NVM_DIR +} + +die () { echo "$@" ; cleanup ; exit 1; } + +: nvm.sh +\. ../../../nvm.sh + +TMP_DIR="$(mktemp -d)" +NVM_DIR="${TMP_DIR}/nvm" +export NVM_DIR +mkdir -p "${NVM_DIR}/alias/lts" || die 'could not create the scratch alias dir' + +# a file outside $NVM_DIR that alias resolution must never reach +printf '%s\n' 'SECRET_OUTSIDE_NVM_DIR' > "${TMP_DIR}/outside" + +# legitimate aliases, to prove the guard does not reject the names nvm uses +printf '%s\n' 'v1.2.3' > "${NVM_DIR}/alias/default" +printf '%s\n' 'v4.5.6' > "${NVM_DIR}/alias/lts/iron" + +for TRAVERSAL in '../../outside' 'lts/../../../outside' '..' 'lts/..' 'a/../../outside'; do + OUTPUT="$(nvm_alias "${TRAVERSAL}" 2>/dev/null)" + EXIT_CODE=$? + [ "_${EXIT_CODE}" = '_3' ] || die "'nvm_alias ${TRAVERSAL}' exited with ${EXIT_CODE}, expected 3" + [ -z "${OUTPUT}" ] || die "'nvm_alias ${TRAVERSAL}' produced output: ${OUTPUT}" + # capture stderr through a file, and match rather than compare: ksh 93u+ has + # no `local`, so it interleaves its own noise with anything nvm.sh writes + nvm_alias "${TRAVERSAL}" >/dev/null 2>"${TMP_DIR}/stderr" + grep -qF "invalid alias name: ${TRAVERSAL}" "${TMP_DIR}/stderr" \ + || die "'nvm_alias ${TRAVERSAL}' produced stderr: $(cat "${TMP_DIR}/stderr")" +done + +# the disclosure path the advisory reports: resolution must not echo the file +for TRAVERSAL in '../../outside' 'lts/../../../outside'; do + OUTPUT="$(nvm_resolve_alias "${TRAVERSAL}" 2>&1)" + case "${OUTPUT}" in + *SECRET_OUTSIDE_NVM_DIR*) die "'nvm_resolve_alias ${TRAVERSAL}' leaked a file outside \$NVM_DIR" ;; + esac +done + +OUTPUT="$(nvm_alias default)" +[ "_${OUTPUT}" = '_v1.2.3' ] || die "'nvm_alias default' produced \"${OUTPUT}\", expected v1.2.3" + +OUTPUT="$(nvm_alias lts/iron)" +[ "_${OUTPUT}" = '_v4.5.6' ] || die "'nvm_alias lts/iron' produced \"${OUTPUT}\", expected v4.5.6" + +for BAD_VERSION in '../../outside' 'v1.2.3/../../..' '..'; do + OUTPUT="$(nvm_version_path "${BAD_VERSION}" 2>/dev/null)" + EXIT_CODE=$? + [ "_${EXIT_CODE}" = '_3' ] || die "'nvm_version_path ${BAD_VERSION}' exited with ${EXIT_CODE}, expected 3" + [ -z "${OUTPUT}" ] || die "'nvm_version_path ${BAD_VERSION}' produced output: ${OUTPUT}" +done + +OUTPUT="$(nvm_version_path v18.0.0)" +[ "_${OUTPUT}" = "_${NVM_DIR}/versions/node/v18.0.0" ] || die "'nvm_version_path v18.0.0' produced \"${OUTPUT}\"" + +cleanup