mirror of
https://github.com/nvm-sh/nvm.git
synced 2026-10-26 00:14:32 +08:00
[Fix] nvm_install_binary, nvm_install_source: only use a tarball that nvm_download_artifact put in the cache
`nvm_install_binary` and `nvm_install_source` took the tarball path from the last line of `nvm_download_artifact`'s output via `| tail -1`. That pipeline's exit status is `tail`'s, so a failed download went unnoticed, and the downloader's own stdout was not separated from the path: anything it printed (eg, from a curl `write-out` setting) could be the last line. If that line happened to name an existing file relative to the current directory, it was extracted and installed as if it were the downloaded tarball, instead of the install failing. `nvm_download_artifact` now sends the downloader's output to stderr, so its stdout is only ever the tarball path. Both install functions capture its output directly and honor its exit status, and accept the result only if it is an existing file inside nvm's cache directory, via a new `nvm_is_cached_artifact` helper Discovered by Anthropic's OSS Scanner, as ANT-2026-DESJ2ESZ; the scanner reported it as a vulnerability, but it is not one.
This commit is contained in:
21
nvm.sh
21
nvm.sh
@@ -2974,8 +2974,8 @@ nvm_install_binary() {
|
||||
PROGRESS_BAR="--progress-bar"
|
||||
fi
|
||||
nvm_echo "Downloading and installing ${NODE_OR_IOJS-} ${VERSION}..."
|
||||
TARBALL="$(PROGRESS_BAR="${PROGRESS_BAR}" nvm_download_artifact "${FLAVOR}" binary "${TYPE-}" "${VERSION}" | command tail -1)"
|
||||
if [ -f "${TARBALL}" ]; then
|
||||
TARBALL="$(PROGRESS_BAR="${PROGRESS_BAR}" nvm_download_artifact "${FLAVOR}" binary "${TYPE-}" "${VERSION}")" || TARBALL=''
|
||||
if nvm_is_cached_artifact "${TARBALL}"; then
|
||||
TMPDIR="$(dirname "${TARBALL}")/files"
|
||||
fi
|
||||
|
||||
@@ -3169,7 +3169,8 @@ nvm_download_artifact() {
|
||||
command rm -rf "${TARBALL}"
|
||||
fi
|
||||
nvm_err "Downloading ${TARBALL_URL}..."
|
||||
nvm_download -L -C - "${PROGRESS_BAR}" "${TARBALL_URL}" -o "${TARBALL}" || {
|
||||
# stdout is reserved for the tarball path, so nothing the downloader prints may reach it
|
||||
nvm_download -L -C - "${PROGRESS_BAR}" "${TARBALL_URL}" -o "${TARBALL}" >&2 || {
|
||||
command rm -rf "${TARBALL}" "${tmpdir}"
|
||||
nvm_err "download from ${TARBALL_URL} failed"
|
||||
return 4
|
||||
@@ -3189,6 +3190,14 @@ nvm_download_artifact() {
|
||||
nvm_echo "${TARBALL}"
|
||||
}
|
||||
|
||||
# a tarball path from nvm_download_artifact is only usable if it is an existing file in nvm's cache
|
||||
nvm_is_cached_artifact() {
|
||||
case "${1-}" in
|
||||
"$(nvm_cache_dir)"/*) [ -f "${1}" ] ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# args: nvm_os, version, tarball, tmpdir
|
||||
nvm_extract_tarball() {
|
||||
if [ "$#" -ne 4 ]; then
|
||||
@@ -3376,8 +3385,8 @@ nvm_install_source() {
|
||||
|
||||
nvm_is_zsh && setopt local_options shwordsplit
|
||||
|
||||
TARBALL="$(PROGRESS_BAR="${PROGRESS_BAR}" nvm_download_artifact "${FLAVOR}" source "${TYPE}" "${VERSION}" | command tail -1)" && \
|
||||
[ -f "${TARBALL}" ] && \
|
||||
TARBALL="$(PROGRESS_BAR="${PROGRESS_BAR}" nvm_download_artifact "${FLAVOR}" source "${TYPE}" "${VERSION}")" && \
|
||||
nvm_is_cached_artifact "${TARBALL}" && \
|
||||
TMPDIR="$(dirname "${TARBALL}")/files" && \
|
||||
if ! (
|
||||
# shellcheck disable=SC2086
|
||||
@@ -5443,7 +5452,7 @@ nvm() {
|
||||
nvm_ls_remote nvm_ls_remote_iojs nvm_ls_remote_index_tab nvm_get_remote_aliases \
|
||||
nvm_ls nvm_remote_version nvm_remote_versions \
|
||||
nvm_install_binary nvm_install_source nvm_clang_version \
|
||||
nvm_get_mirror nvm_get_download_slug nvm_download_artifact \
|
||||
nvm_get_mirror nvm_get_download_slug nvm_download_artifact nvm_is_cached_artifact \
|
||||
nvm_install_npm_if_needed nvm_use_if_needed nvm_check_file_permissions \
|
||||
nvm_print_versions nvm_compute_checksum \
|
||||
nvm_get_checksum_binary \
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/bin/sh
|
||||
|
||||
WORK="${PWD}/nvm-download-artifact-stdout-work.$$"
|
||||
|
||||
cleanup() {
|
||||
command rm -rf "${WORK}"
|
||||
NVM_DIR="${OLD_NVM_DIR}"
|
||||
unset -f cleanup die nvm_download nvm_get_checksum nvm_compare_checksum
|
||||
}
|
||||
die () { echo "$@" ; cleanup ; exit 1; }
|
||||
|
||||
: nvm.sh
|
||||
\. ../../../nvm.sh
|
||||
|
||||
\. ../../common.sh
|
||||
|
||||
OLD_NVM_DIR="${NVM_DIR}"
|
||||
NVM_DIR="${WORK}/nvm"
|
||||
command mkdir -p "${NVM_DIR}"
|
||||
|
||||
nvm_get_checksum() { echo checksum; }
|
||||
nvm_compare_checksum() { return 0; }
|
||||
|
||||
# a downloader that prints to stdout, as curl does with a `write-out` setting
|
||||
nvm_download() {
|
||||
while [ $# -gt 0 ]; do
|
||||
if [ "$1" = '-o' ]; then
|
||||
echo tarball > "$2"
|
||||
fi
|
||||
shift
|
||||
done
|
||||
echo 200
|
||||
}
|
||||
|
||||
try nvm_download_artifact node binary std 8.0.0
|
||||
[ "${CAPTURED_EXIT_CODE}" = 0 ] || die "nvm_download_artifact failed; got exit code ${CAPTURED_EXIT_CODE}"
|
||||
case "${CAPTURED_STDOUT}" in
|
||||
"$(nvm_cache_dir)"/bin/*) ;;
|
||||
*) die "nvm_download_artifact printed more than the cached tarball path; got '${CAPTURED_STDOUT}'" ;;
|
||||
esac
|
||||
[ -f "${CAPTURED_STDOUT}" ] || die "nvm_download_artifact printed a path that does not exist: '${CAPTURED_STDOUT}'"
|
||||
|
||||
command rm -rf "$(nvm_cache_dir)"
|
||||
|
||||
nvm_download() {
|
||||
echo 404
|
||||
return 22
|
||||
}
|
||||
|
||||
try nvm_download_artifact node binary std 8.0.0
|
||||
[ "${CAPTURED_EXIT_CODE}" = 4 ] || die "a failed download should exit with 4; got ${CAPTURED_EXIT_CODE}"
|
||||
[ -z "${CAPTURED_STDOUT}" ] || die "a failed download should print nothing to stdout; got '${CAPTURED_STDOUT}'"
|
||||
|
||||
cleanup
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/bin/sh
|
||||
|
||||
WORK="${PWD}/nvm-tarball-from-cache-work.$$"
|
||||
EXTRACT_LOG="${WORK}/extract.log"
|
||||
|
||||
cleanup() {
|
||||
command rm -rf "${WORK}"
|
||||
NVM_DIR="${OLD_NVM_DIR}"
|
||||
unset -f cleanup die nvm_download_artifact nvm_extract_tarball run_in_project
|
||||
}
|
||||
die () { echo "$@" ; cleanup ; exit 1; }
|
||||
|
||||
: nvm.sh
|
||||
\. ../../../nvm.sh
|
||||
|
||||
OLD_NVM_DIR="${NVM_DIR}"
|
||||
NVM_DIR="${WORK}/nvm"
|
||||
CACHED_TARBALL="$(nvm_cache_dir)/bin/node-v8.0.0-linux-x64/node-v8.0.0-linux-x64.tar.xz"
|
||||
|
||||
command mkdir -p "${WORK}/project" "$(dirname "${CACHED_TARBALL}")"
|
||||
echo tarball > "${CACHED_TARBALL}"
|
||||
# a file in the current directory named like a line a downloader might print
|
||||
echo not-a-cached-tarball > "${WORK}/project/404"
|
||||
|
||||
nvm_extract_tarball() {
|
||||
echo "$3" >> "${EXTRACT_LOG}"
|
||||
return 1
|
||||
}
|
||||
|
||||
run_in_project() {
|
||||
command rm -f "${EXTRACT_LOG}"
|
||||
(
|
||||
cd "${WORK}/project" || exit 1
|
||||
"$@" < /dev/null > /dev/null 2>&1
|
||||
)
|
||||
[ -f "${EXTRACT_LOG}" ] && command cat "${EXTRACT_LOG}"
|
||||
:
|
||||
}
|
||||
|
||||
# a failed download's last stdout line must not be taken as the tarball
|
||||
nvm_download_artifact() { echo 404; return 4; }
|
||||
|
||||
[ -z "$(run_in_project nvm_install_binary node std 8.0.0 1)" ] || die 'nvm_install_binary extracted the output of a failed download'
|
||||
[ -z "$(run_in_project nvm_install_source node std 8.0.0)" ] || die 'nvm_install_source extracted the output of a failed download'
|
||||
|
||||
# nor may any path outside nvm's cache
|
||||
nvm_download_artifact() { echo 404; }
|
||||
|
||||
[ -z "$(run_in_project nvm_install_binary node std 8.0.0 1)" ] || die 'nvm_install_binary extracted a tarball outside the cache'
|
||||
[ -z "$(run_in_project nvm_install_source node std 8.0.0)" ] || die 'nvm_install_source extracted a tarball outside the cache'
|
||||
|
||||
# a tarball in nvm's cache is still extracted
|
||||
nvm_download_artifact() { nvm_echo "${CACHED_TARBALL}"; }
|
||||
|
||||
[ "$(run_in_project nvm_install_binary node std 8.0.0 1)" = "${CACHED_TARBALL}" ] || die 'nvm_install_binary did not extract the cached tarball'
|
||||
echo tarball > "${CACHED_TARBALL}"
|
||||
[ "$(run_in_project nvm_install_source node std 8.0.0)" = "${CACHED_TARBALL}" ] || die 'nvm_install_source did not extract the cached tarball'
|
||||
|
||||
cleanup
|
||||
84
test/fast/Unit tests/nvm_is_cached_artifact
Executable file
84
test/fast/Unit tests/nvm_is_cached_artifact
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/bin/sh
|
||||
|
||||
WORK="${PWD}/nvm-is-cached-artifact-work.$$"
|
||||
|
||||
cleanup() {
|
||||
command rm -rf "${WORK}"
|
||||
NVM_DIR="${OLD_NVM_DIR}"
|
||||
unset -f cleanup die check
|
||||
}
|
||||
die () { echo "$@" ; cleanup ; exit 1; }
|
||||
|
||||
\. ../../../nvm.sh
|
||||
|
||||
OLD_NVM_DIR="${NVM_DIR}"
|
||||
|
||||
check() {
|
||||
EXPECTED="${1}"
|
||||
shift
|
||||
ACTUAL=0
|
||||
nvm_is_cached_artifact "$@" || ACTUAL=$?
|
||||
[ "${ACTUAL}" = "${EXPECTED}" ] || die "nvm_is_cached_artifact '$*' (NVM_DIR '${NVM_DIR}'): expected exit code ${EXPECTED}, got ${ACTUAL}"
|
||||
}
|
||||
|
||||
NVM_DIR="${WORK}/nvm"
|
||||
CACHE="$(nvm_cache_dir)"
|
||||
[ "${CACHE}" = "${NVM_DIR}/.cache" ] || die "expected cache dir '${NVM_DIR}/.cache', got '${CACHE}'"
|
||||
|
||||
command mkdir -p "${CACHE}/bin/node-v8.0.0-linux-x64" "${CACHE}/src/node-v8.0.0/files" "${WORK}/project" "${WORK}/nvm/.cache-other" "${WORK}/outside"
|
||||
echo tarball > "${CACHE}/bin/node-v8.0.0-linux-x64/node-v8.0.0-linux-x64.tar.xz"
|
||||
echo tarball > "${CACHE}/top-level.tar.gz"
|
||||
echo not-a-cached-tarball > "${WORK}/project/404"
|
||||
echo not-a-cached-tarball > "${WORK}/outside/node-v8.0.0.tar.xz"
|
||||
echo not-a-cached-tarball > "${WORK}/nvm/.cache-other/node-v8.0.0.tar.xz"
|
||||
echo not-a-cached-tarball > "${WORK}/nvm/.cachefile"
|
||||
|
||||
# existing files inside the cache are accepted, at any depth
|
||||
check 0 "${CACHE}/bin/node-v8.0.0-linux-x64/node-v8.0.0-linux-x64.tar.xz"
|
||||
check 0 "${CACHE}/top-level.tar.gz"
|
||||
|
||||
# no argument, or an empty one, is rejected
|
||||
check 1
|
||||
check 1 ''
|
||||
|
||||
# paths in the cache that are not existing regular files are rejected
|
||||
check 1 "${CACHE}"
|
||||
check 1 "${CACHE}/"
|
||||
check 1 "${CACHE}/bin"
|
||||
check 1 "${CACHE}/src/node-v8.0.0/files"
|
||||
check 1 "${CACHE}/bin/node-v8.0.0-linux-x64/missing.tar.xz"
|
||||
|
||||
# existing files outside the cache are rejected
|
||||
check 1 "${WORK}/outside/node-v8.0.0.tar.xz"
|
||||
check 1 "${NVM_DIR}/.cachefile"
|
||||
check 1 "${NVM_DIR}/.cache-other/node-v8.0.0.tar.xz"
|
||||
|
||||
# a downloader's stray output that names a file relative to the current directory is rejected
|
||||
(
|
||||
cd "${WORK}/project" || exit 2
|
||||
[ -f 404 ] || exit 2
|
||||
nvm_is_cached_artifact 404 && exit 3
|
||||
exit 0
|
||||
)
|
||||
RESULT=$?
|
||||
[ "${RESULT}" = 0 ] || die "nvm_is_cached_artifact '404' in a directory containing '404': expected rejection, got subshell exit code ${RESULT}"
|
||||
|
||||
# a relative path that happens to reach a cached file is still rejected, since it is not under the cache dir
|
||||
(
|
||||
cd "${CACHE}" || exit 2
|
||||
[ -f top-level.tar.gz ] || exit 2
|
||||
nvm_is_cached_artifact top-level.tar.gz && exit 3
|
||||
exit 0
|
||||
)
|
||||
RESULT=$?
|
||||
[ "${RESULT}" = 0 ] || die "nvm_is_cached_artifact 'top-level.tar.gz' from inside the cache dir: expected rejection, got subshell exit code ${RESULT}"
|
||||
|
||||
# glob characters in NVM_DIR are matched literally, not as a pattern
|
||||
NVM_DIR="${WORK}/n*"
|
||||
command mkdir -p "$(nvm_cache_dir)"
|
||||
echo tarball > "$(nvm_cache_dir)/glob.tar.xz"
|
||||
echo not-a-cached-tarball > "${WORK}/nvm/.cache/glob.tar.xz"
|
||||
check 0 "${WORK}/n*/.cache/glob.tar.xz"
|
||||
check 1 "${WORK}/nvm/.cache/glob.tar.xz"
|
||||
|
||||
cleanup
|
||||
Reference in New Issue
Block a user