The header was passed as a `--header` argument, so it was readable by anyone who could see the downloader's command line. curl now reads it as `header = "…"` from a config on stdin (`--config -`); the sanitized value can contain neither `"` nor `\` nor a line break, so it cannot escape the quoted string or add another option. GNU Wget only reads a config file that is a regular file, so a subshell writes the header to a `mktemp` file in `$TMPDIR` (mode 0600), after a copy of the user's `$WGETRC` or `~/.wgetrc`, which that file replaces, and points `WGETRC` at it; traps on EXIT, HUP, INT, and TERM remove it, and `WGETRC` never reaches the user's shell. BusyBox wget reads no config file and has no option for one, so it still gets the header as an argument; the README recommends curl on shared machines. Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-540RL2U6.
188 lines
7.9 KiB
Bash
Executable File
188 lines
7.9 KiB
Bash
Executable File
#!/bin/sh
|
|
|
|
WORK="$PWD/nvm_wget_with_auth_header-work.$$"
|
|
OLDHOME="$HOME"
|
|
OLDTMPDIR="${TMPDIR-}"
|
|
|
|
cleanup() {
|
|
unset -f nvm_wget_same_origin
|
|
unset WGETRC MOCK_EXIT MOCK_KILL
|
|
rm -rf "$WORK"
|
|
HOME="$OLDHOME"
|
|
export HOME
|
|
TMPDIR="$OLDTMPDIR"
|
|
export TMPDIR
|
|
}
|
|
die () { echo "$@" ; cleanup ; exit 1; }
|
|
|
|
\. ../../../nvm.sh
|
|
|
|
mkdir -p "$WORK/home" "$WORK/tmp" || die 'unable to create the work directory'
|
|
HOME="$WORK/home"
|
|
export HOME
|
|
TMPDIR="$WORK/tmp"
|
|
export TMPDIR
|
|
unset WGETRC
|
|
|
|
# records what wget would be run with; the real one runs `command wget`
|
|
nvm_wget_same_origin() {
|
|
: > "$WORK/args"
|
|
for arg in "$@"; do
|
|
printf '%s\n' "$arg" >> "$WORK/args"
|
|
done
|
|
printf '%s\n' "${WGETRC-}" > "$WORK/wgetrc-path"
|
|
if [ -n "${WGETRC-}" ] && [ -f "${WGETRC}" ]; then
|
|
cat "${WGETRC}" > "$WORK/wgetrc"
|
|
ls -l "${WGETRC}" | cut -c1-10 > "$WORK/wgetrc-mode"
|
|
else
|
|
: > "$WORK/wgetrc"
|
|
: > "$WORK/wgetrc-mode"
|
|
fi
|
|
if [ -n "${MOCK_KILL-}" ]; then
|
|
# the subshell running this function is the parent of this `sh`
|
|
sh -c "kill -${MOCK_KILL} \$PPID"
|
|
return 0
|
|
fi
|
|
return "${MOCK_EXIT:-0}"
|
|
}
|
|
|
|
reset_logs() {
|
|
rm -f "$WORK/args" "$WORK/wgetrc-path" "$WORK/wgetrc" "$WORK/wgetrc-mode"
|
|
}
|
|
|
|
assert_cleaned_up() {
|
|
[ -n "$(cat "$WORK/wgetrc-path")" ] || die "$1: expected WGETRC to be set for wget, got it empty"
|
|
[ ! -e "$(cat "$WORK/wgetrc-path")" ] || die "$1: expected the config file to be removed, got $(cat "$WORK/wgetrc-path") left behind"
|
|
[ -z "$(ls -A "$WORK/tmp")" ] || die "$1: expected \$TMPDIR to be empty, got: $(ls -A "$WORK/tmp")"
|
|
}
|
|
|
|
# given no wgetrc of the user's
|
|
reset_logs
|
|
nvm_wget_with_auth_header 'Bearer test-token' -q -O - 'https://example.com/dist/index.tab'
|
|
EXIT_CODE=$?
|
|
[ "$EXIT_CODE" = 0 ] || die "no wgetrc: expected exit code 0, got $EXIT_CODE"
|
|
# then the header is the only line of the config
|
|
[ "$(cat "$WORK/wgetrc")" = 'header = Authorization: Bearer test-token' ] || die "no wgetrc: expected only the header in the config, got >$(cat "$WORK/wgetrc")<"
|
|
# and the remaining arguments, without the header, go to nvm_wget_same_origin
|
|
EXPECTED_ARGS='-q
|
|
-O
|
|
-
|
|
https://example.com/dist/index.tab'
|
|
[ "$(cat "$WORK/args")" = "$EXPECTED_ARGS" ] || die "no wgetrc: expected args >$EXPECTED_ARGS<, got >$(cat "$WORK/args")<"
|
|
! grep -q 'test-token' "$WORK/args" || die "no wgetrc: expected the credential not to be an argument, got >$(cat "$WORK/args")<"
|
|
# in a file in $TMPDIR that only the user can read
|
|
case "$(cat "$WORK/wgetrc-path")" in
|
|
"$WORK/tmp/nvm-wgetrc."*) ;;
|
|
*) die "no wgetrc: expected the config file in \$TMPDIR, got $(cat "$WORK/wgetrc-path")" ;;
|
|
esac
|
|
[ "$(cat "$WORK/wgetrc-mode")" = '-rw-------' ] || die "no wgetrc: expected mode -rw-------, got $(cat "$WORK/wgetrc-mode")"
|
|
assert_cleaned_up 'no wgetrc'
|
|
# and WGETRC does not reach the caller
|
|
[ -z "${WGETRC+set}" ] || die "no wgetrc: expected WGETRC to stay unset, got >${WGETRC}<"
|
|
|
|
# given wget's exit code
|
|
reset_logs
|
|
MOCK_EXIT=8
|
|
nvm_wget_with_auth_header 'Bearer test-token' 'https://example.com/dist/index.tab'
|
|
EXIT_CODE=$?
|
|
unset MOCK_EXIT
|
|
# then it is returned
|
|
[ "$EXIT_CODE" = 8 ] || die "wget failure: expected exit code 8, got $EXIT_CODE"
|
|
assert_cleaned_up 'wget failure'
|
|
|
|
# given the user's ~/.wgetrc, with no trailing newline
|
|
reset_logs
|
|
printf 'tries = 7' > "$HOME/.wgetrc"
|
|
nvm_wget_with_auth_header 'Bearer test-token' 'https://example.com/dist/index.tab' || die 'with ~/.wgetrc: expected exit code 0, got nonzero'
|
|
# then its settings are kept, and the header is on a line of its own after them
|
|
EXPECTED_RC='tries = 7
|
|
header = Authorization: Bearer test-token'
|
|
[ "$(cat "$WORK/wgetrc")" = "$EXPECTED_RC" ] || die "with ~/.wgetrc: expected >$EXPECTED_RC<, got >$(cat "$WORK/wgetrc")<"
|
|
assert_cleaned_up 'with ~/.wgetrc'
|
|
[ "$(cat "$HOME/.wgetrc")" = 'tries = 7' ] || die "with ~/.wgetrc: expected ~/.wgetrc unchanged, got >$(cat "$HOME/.wgetrc")<"
|
|
|
|
# given $WGETRC naming the user's config, alongside a ~/.wgetrc
|
|
reset_logs
|
|
printf 'timeout = 3\n' > "$WORK/custom-wgetrc"
|
|
WGETRC="$WORK/custom-wgetrc"
|
|
export WGETRC
|
|
nvm_wget_with_auth_header 'Bearer test-token' 'https://example.com/dist/index.tab' || die 'with $WGETRC: expected exit code 0, got nonzero'
|
|
# then $WGETRC is the one copied, not ~/.wgetrc
|
|
EXPECTED_RC='timeout = 3
|
|
|
|
header = Authorization: Bearer test-token'
|
|
[ "$(cat "$WORK/wgetrc")" = "$EXPECTED_RC" ] || die "with \$WGETRC: expected >$EXPECTED_RC<, got >$(cat "$WORK/wgetrc")<"
|
|
assert_cleaned_up 'with $WGETRC'
|
|
# and the caller's WGETRC is unchanged
|
|
[ "${WGETRC-}" = "$WORK/custom-wgetrc" ] || die "with \$WGETRC: expected WGETRC to stay $WORK/custom-wgetrc, got >${WGETRC-}<"
|
|
[ "$(cat "$WORK/custom-wgetrc")" = 'timeout = 3' ] || die "with \$WGETRC: expected it unchanged, got >$(cat "$WORK/custom-wgetrc")<"
|
|
|
|
# given $WGETRC naming a file that does not exist
|
|
reset_logs
|
|
WGETRC="$WORK/missing-wgetrc"
|
|
nvm_wget_with_auth_header 'Bearer test-token' 'https://example.com/dist/index.tab' || die 'with a missing $WGETRC: expected exit code 0, got nonzero'
|
|
# then only the header is in the config; ~/.wgetrc is not used in its place
|
|
[ "$(cat "$WORK/wgetrc")" = 'header = Authorization: Bearer test-token' ] || die "with a missing \$WGETRC: expected only the header, got >$(cat "$WORK/wgetrc")<"
|
|
assert_cleaned_up 'with a missing $WGETRC'
|
|
unset WGETRC
|
|
rm -f "$HOME/.wgetrc"
|
|
|
|
# given a header value with shell metacharacters
|
|
reset_logs
|
|
nvm_wget_with_auth_header 'Bearer $HOME `id` $(id) *' 'https://example.com/dist/index.tab' || die 'metacharacters: expected exit code 0, got nonzero'
|
|
# then it is written verbatim
|
|
[ "$(cat "$WORK/wgetrc")" = 'header = Authorization: Bearer $HOME `id` $(id) *' ] || die "metacharacters: expected the header verbatim, got >$(cat "$WORK/wgetrc")<"
|
|
assert_cleaned_up 'metacharacters'
|
|
|
|
# given that the temporary file cannot be created
|
|
reset_logs
|
|
TMPDIR="$WORK/nonexistent"
|
|
ERR="$(nvm_wget_with_auth_header 'Bearer test-token' 'https://example.com/dist/index.tab' 2>&1 >/dev/null)"
|
|
EXIT_CODE=$?
|
|
TMPDIR="$WORK/tmp"
|
|
# then it fails, says why, and never runs wget
|
|
[ "$EXIT_CODE" = 1 ] || die "no temporary file: expected exit code 1, got $EXIT_CODE"
|
|
case "$ERR" in
|
|
*'unable to create a temporary file for $NVM_AUTH_HEADER'*) ;;
|
|
*) die "no temporary file: expected an error about the temporary file, got >$ERR<" ;;
|
|
esac
|
|
[ ! -e "$WORK/args" ] || die "no temporary file: expected wget not to run, got args >$(cat "$WORK/args")<"
|
|
# and stops there, before trying to write the config
|
|
[ "$(printf '%s\n' "$ERR" | tail -n 1)" = 'unable to create a temporary file for $NVM_AUTH_HEADER' ] || die "no temporary file: expected its error to be the last thing printed, got >$ERR<"
|
|
|
|
# given that $TMPDIR is unset or empty
|
|
for TMPDIR_STATE in unset empty; do
|
|
reset_logs
|
|
if [ "$TMPDIR_STATE" = unset ]; then
|
|
unset TMPDIR
|
|
else
|
|
TMPDIR=''
|
|
fi
|
|
nvm_wget_with_auth_header 'Bearer test-token' 'https://example.com/dist/index.tab'
|
|
EXIT_CODE=$?
|
|
TMPDIR="$WORK/tmp"
|
|
export TMPDIR
|
|
# then the config file goes in /tmp, and is removed afterwards
|
|
[ "$EXIT_CODE" = 0 ] || die "$TMPDIR_STATE \$TMPDIR: expected exit code 0, got $EXIT_CODE"
|
|
case "$(cat "$WORK/wgetrc-path")" in
|
|
/tmp/nvm-wgetrc.*) ;;
|
|
*) die "$TMPDIR_STATE \$TMPDIR: expected the config file in /tmp, got >$(cat "$WORK/wgetrc-path")<" ;;
|
|
esac
|
|
[ "$(cat "$WORK/wgetrc")" = 'header = Authorization: Bearer test-token' ] || die "$TMPDIR_STATE \$TMPDIR: expected only the header in the config, got >$(cat "$WORK/wgetrc")<"
|
|
[ ! -e "$(cat "$WORK/wgetrc-path")" ] || die "$TMPDIR_STATE \$TMPDIR: expected the config file to be removed, got $(cat "$WORK/wgetrc-path") left behind"
|
|
done
|
|
|
|
# given that the download is interrupted
|
|
for SIGNAL_CODE in HUP:129 INT:130 TERM:143; do
|
|
reset_logs
|
|
MOCK_KILL="${SIGNAL_CODE%%:*}"
|
|
nvm_wget_with_auth_header 'Bearer test-token' 'https://example.com/dist/index.tab'
|
|
EXIT_CODE=$?
|
|
unset MOCK_KILL
|
|
# then the config file is removed, and the signal's exit code is returned
|
|
[ "$EXIT_CODE" = "${SIGNAL_CODE#*:}" ] || die "SIG${SIGNAL_CODE%%:*}: expected exit code ${SIGNAL_CODE#*:}, got $EXIT_CODE"
|
|
assert_cleaned_up "SIG${SIGNAL_CODE%%:*}"
|
|
done
|
|
|
|
cleanup
|