Skip to content

Commit 072622c

Browse files
committed
[Fix] nvm_alias, nvm_version_path: reject .. path components
`nvm_alias` concatenated the requested name onto `$NVM_DIR/alias` and read whatever that resolved to, so a name with a `..` component escaped the alias dir - under the default layout, `../../.npmrc` reaches `$HOME/.npmrc`. `nvm_print_alias_file` echoes every non-comment line of what it opens, and `nvm use` reports the first one in its "is not yet installed" error, so an untrusted `.nvmrc` could disclose any file the invoking user can read. The write side already rejected `..` in an alias name (9275c5b); apply the same check on the read side, and to `nvm_version_path`, which composes a version into a path with no check of its own. Slashes stay legal, since `lts/iron` and the `lts/*` alias file depend on them.
1 parent 60a8da7 commit 072622c

2 files changed

Lines changed: 82 additions & 0 deletions

File tree

‎nvm.sh‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,12 @@ nvm_alias_path() {
800800
nvm_version_path() {
801801
local VERSION
802802
VERSION="${1-}"
803+
case "/${VERSION}/" in
804+
*/../*)
805+
nvm_err "invalid version: ${VERSION}"
806+
return 3
807+
;;
808+
esac
803809
if [ -z "${VERSION}" ]; then
804810
nvm_err 'version is required'
805811
return 3
@@ -1498,6 +1504,15 @@ nvm_alias() {
14981504
return 2
14991505
fi
15001506

1507+
# slashes are legal (eg `lts/iron`), but a `..` component would read outside
1508+
# the alias dir; `nvm_make_alias` rejects the same shape on the write side
1509+
case "/${ALIAS}/" in
1510+
*/../*)
1511+
nvm_err "invalid alias name: ${ALIAS}"
1512+
return 3
1513+
;;
1514+
esac
1515+
15011516
local NVM_ALIAS_PATH
15021517
NVM_ALIAS_PATH="$(nvm_alias_path)/${ALIAS}"
15031518
if [ ! -f "${NVM_ALIAS_PATH}" ]; then
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/bin/sh
2+
3+
ORIG_NVM_DIR="${NVM_DIR}"
4+
TMP_DIR=''
5+
6+
cleanup () {
7+
case "${TMP_DIR}" in
8+
/*) rm -rf "${TMP_DIR}" ;;
9+
esac
10+
NVM_DIR="${ORIG_NVM_DIR}"
11+
export NVM_DIR
12+
}
13+
14+
die () { echo "$@" ; cleanup ; exit 1; }
15+
16+
: nvm.sh
17+
\. ../../../nvm.sh
18+
19+
TMP_DIR="$(mktemp -d)"
20+
NVM_DIR="${TMP_DIR}/nvm"
21+
export NVM_DIR
22+
mkdir -p "${NVM_DIR}/alias/lts" || die 'could not create the scratch alias dir'
23+
24+
# a file outside $NVM_DIR that alias resolution must never reach
25+
printf '%s\n' 'SECRET_OUTSIDE_NVM_DIR' > "${TMP_DIR}/outside"
26+
27+
# legitimate aliases, to prove the guard does not reject the names nvm uses
28+
printf '%s\n' 'v1.2.3' > "${NVM_DIR}/alias/default"
29+
printf '%s\n' 'v4.5.6' > "${NVM_DIR}/alias/lts/iron"
30+
31+
for TRAVERSAL in '../../outside' 'lts/../../../outside' '..' 'lts/..' 'a/../../outside'; do
32+
OUTPUT="$(nvm_alias "${TRAVERSAL}" 2>/dev/null)"
33+
EXIT_CODE=$?
34+
[ "_${EXIT_CODE}" = '_3' ] || die "'nvm_alias ${TRAVERSAL}' exited with ${EXIT_CODE}, expected 3"
35+
[ -z "${OUTPUT}" ] || die "'nvm_alias ${TRAVERSAL}' produced output: ${OUTPUT}"
36+
# capture stderr through a file, and match rather than compare: ksh 93u+ has
37+
# no `local`, so it interleaves its own noise with anything nvm.sh writes
38+
nvm_alias "${TRAVERSAL}" >/dev/null 2>"${TMP_DIR}/stderr"
39+
grep -qF "invalid alias name: ${TRAVERSAL}" "${TMP_DIR}/stderr" \
40+
|| die "'nvm_alias ${TRAVERSAL}' produced stderr: $(cat "${TMP_DIR}/stderr")"
41+
done
42+
43+
# the disclosure path the advisory reports: resolution must not echo the file
44+
for TRAVERSAL in '../../outside' 'lts/../../../outside'; do
45+
OUTPUT="$(nvm_resolve_alias "${TRAVERSAL}" 2>&1)"
46+
case "${OUTPUT}" in
47+
*SECRET_OUTSIDE_NVM_DIR*) die "'nvm_resolve_alias ${TRAVERSAL}' leaked a file outside \$NVM_DIR" ;;
48+
esac
49+
done
50+
51+
OUTPUT="$(nvm_alias default)"
52+
[ "_${OUTPUT}" = '_v1.2.3' ] || die "'nvm_alias default' produced \"${OUTPUT}\", expected v1.2.3"
53+
54+
OUTPUT="$(nvm_alias lts/iron)"
55+
[ "_${OUTPUT}" = '_v4.5.6' ] || die "'nvm_alias lts/iron' produced \"${OUTPUT}\", expected v4.5.6"
56+
57+
for BAD_VERSION in '../../outside' 'v1.2.3/../../..' '..'; do
58+
OUTPUT="$(nvm_version_path "${BAD_VERSION}" 2>/dev/null)"
59+
EXIT_CODE=$?
60+
[ "_${EXIT_CODE}" = '_3' ] || die "'nvm_version_path ${BAD_VERSION}' exited with ${EXIT_CODE}, expected 3"
61+
[ -z "${OUTPUT}" ] || die "'nvm_version_path ${BAD_VERSION}' produced output: ${OUTPUT}"
62+
done
63+
64+
OUTPUT="$(nvm_version_path v18.0.0)"
65+
[ "_${OUTPUT}" = "_${NVM_DIR}/versions/node/v18.0.0" ] || die "'nvm_version_path v18.0.0' produced \"${OUTPUT}\""
66+
67+
cleanup

0 commit comments

Comments
 (0)