diff --git a/libexec/pyenv-version-file-read b/libexec/pyenv-version-file-read index faaf1596..7865a730 100755 --- a/libexec/pyenv-version-file-read +++ b/libexec/pyenv-version-file-read @@ -5,18 +5,33 @@ set -e VERSION_FILE="$1" +function is_version_safe() { + # As needed, check that the constructed path exists as a child path of PYENV_ROOT/versions + version="$1" + if [[ "$version" == ".." || "$version" == */* ]]; then + # Sanity check the value of version to prevent malicious path-traversal + ( + cd "$PYENV_ROOT/versions/$version" &>/dev/null || exit 1 + [[ "$PWD" == "$PYENV_ROOT/versions/"* ]] + ) + return $? + else + return 0 + fi +} + if [ -s "$VERSION_FILE" ]; then # Read the first non-whitespace word from the specified version file. # Be careful not to load it whole in case there's something crazy in it. - IFS="${IFS}"$'\r' + IFS="$IFS"$'\r' sep= while read -n 1024 -r version _ || [[ $version ]]; do - if [[ -z $version || $version == \#* ]]; then + if [[ -z "$version" || "$version" == \#* ]]; then # Skip empty lines and comments continue - elif [ "$version" = ".." ] || [[ $version == */* ]]; then - # The version string is used to construct a path and we skip dubious values. - # This prevents issues such as path traversal (CVE-2022-35861). + elif ! is_version_safe "$version"; then + # CVE-2022-35861 allowed arbitrary code execution in some contexts and is mitigated by is_version_safe. + echo "pyenv: invalid version \`$version' ignored in \`$VERSION_FILE'" >&2 continue fi printf "%s%s" "$sep" "$version" diff --git a/test/version-file-read.bats b/test/version-file-read.bats index 18cfe131..acd9d781 100644 --- a/test/version-file-read.bats +++ b/test/version-file-read.bats @@ -83,14 +83,36 @@ IN assert_success "3.9.3:3.8.9:2.7.16" } -@test "skips relative path traversal" { +@test "skips \`..' relative path traversal" { + echo '..' > my-version + run pyenv-version-file-read my-version + assert_failure "pyenv: invalid version \`..' ignored in \`my-version'" +} + +@test "skips glob path traversal" { cat > my-version < my-version + run pyenv-version-file-read my-version + assert_success "${venv}" +} + +@test "skips relative paths that lead outside of versions" { + venv=../3.10.3/envs/test + mkdir -p "${PYENV_ROOT}/versions/${venv}" + echo -n "${venv}" > my-version + run pyenv-version-file-read my-version + assert_failure }