CVE-2023-41040 (Python)

GitPython is a python library used to interact with Git repositories. In order to resolve some git references, GitPython reads files from the `.git` directory, in some places the name of the file being read is provided by the user, GitPython doesn't check if this file is located outside the `.git` directory. This allows an attacker to make GitPython read any file from the system. This vulnerability is present in https://github.com/gitpython-developers/GitPython/blob/1c8310d7cae144f74a671cbe17e51f63a830adbf/git/refs/symbolic.py#L174-L175. That code joins the base directory with a user given string without checking if the final path is located outside the base directory. This vulnerability cannot be used to read the contents of files but could in theory be used to trigger a denial of service for the program. This issue has been addressed in version 3.1.37.

Weakness:
CWE-73: name: External Control of File Name or Path
description: The product allows user input to control or influence paths or file names that are used in filesystem operations.
CWE-22: name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.

Repository: https://github.com/gitpython-developers/GitPython

Vulnerable code:
file_path: git/refs/symbolic.py
snippet:     def _get_ref_info_helper(
        cls, repo: "Repo", ref_path: Union[PathLike, None]
    ) -> Union[Tuple[str, None], Tuple[None, str]]:
        """Return: (str(sha), str(target_ref_path)) if available, the sha the file at
        rela_path points to, or None. target_ref_path is the reference we
        point to, or None"""
        if ".." in str(ref_path):
            raise ValueError(f"Invalid reference '{ref_path}'")
        tokens: Union[None, List[str], Tuple[str, str]] = None
        repodir = _git_dir(repo, ref_path)
        try:
            with open(os.path.join(repodir, str(ref_path)), "rt", encoding="UTF-8") as fp:
                value = fp.read().rstrip()
            # Don't only split on spaces, but on whitespace, which allows to parse lines like
            # 60b64ef992065e2600bfef6187a97f92398a9144                branch 'master' of git-server:/path/to/repo
            tokens = value.split()
            assert len(tokens) != 0
        except OSError:
            # Probably we are just packed, find our entry in the packed refs file
            # NOTE: We are not a symbolic ref if we are in a packed file, as these
            # are excluded explicitly
            for sha, path in cls._iter_packed_refs(repo):
                if path != ref_path:
                    continue
                # sha will be used
                tokens = sha, path
                break
            # END for each packed ref
        # END handle packed refs
        if tokens is None:
            raise ValueError("Reference at %r does not exist" % ref_path)

        # is it a reference ?
        if tokens[0] == "ref:":
            return (None, tokens[1])

        # its a commit
        if repo.re_hexsha_only.match(tokens[0]):
            return (tokens[0], None)

        raise ValueError("Failed to parse reference information from %r" % ref_path)

Fixed code:
file_path: git/refs/symbolic.py
snippet:     def _get_ref_info_helper(
        cls, repo: "Repo", ref_path: Union[PathLike, None]
    ) -> Union[Tuple[str, None], Tuple[None, str]]:
        """Return: (str(sha), str(target_ref_path)) if available, the sha the file at
        rela_path points to, or None. target_ref_path is the reference we
        point to, or None"""
        if ref_path:
            cls._check_ref_name_valid(ref_path)

        tokens: Union[None, List[str], Tuple[str, str]] = None
        repodir = _git_dir(repo, ref_path)
        try:
            with open(os.path.join(repodir, str(ref_path)), "rt", encoding="UTF-8") as fp:
                value = fp.read().rstrip()
            # Don't only split on spaces, but on whitespace, which allows to parse lines like
            # 60b64ef992065e2600bfef6187a97f92398a9144                branch 'master' of git-server:/path/to/repo
            tokens = value.split()
            assert len(tokens) != 0
        except OSError:
            # Probably we are just packed, find our entry in the packed refs file
            # NOTE: We are not a symbolic ref if we are in a packed file, as these
            # are excluded explicitly
            for sha, path in cls._iter_packed_refs(repo):
                if path != ref_path:
                    continue
                # sha will be used
                tokens = sha, path
                break
            # END for each packed ref
        # END handle packed refs
        if tokens is None:
            raise ValueError("Reference at %r does not exist" % ref_path)

file_path: git/refs/symbolic.py
snippet:     @staticmethod
    def _check_ref_name_valid(ref_path: PathLike) -> None:
        # Based on the rules described in https://git-scm.com/docs/git-check-ref-format/#_description
        previous: Union[str, None] = None
        one_before_previous: Union[str, None] = None
        for c in str(ref_path):
            if c in " ~^:?*[\\":
                raise ValueError(
                    f"Invalid reference '{ref_path}': references cannot contain spaces, tildes (~), carets (^),"
                    f" colons (:), question marks (?), asterisks (*), open brackets ([) or backslashes (\\)"
                )
            elif c == ".":
                if previous is None or previous == "/":
                    raise ValueError(
                        f"Invalid reference '{ref_path}': references cannot start with a period (.) or contain '/.'"
                    )
                elif previous == ".":
                    raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '..'")
            elif c == "/":
                if previous == "/":
                    raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '//'")
                elif previous is None:
                    raise ValueError(
                        f"Invalid reference '{ref_path}': references cannot start with forward slashes '/'"
                    )
            elif c == "{" and previous == "@":
                raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '@{{'")
            elif ord(c) < 32 or ord(c) == 127:
                raise ValueError(f"Invalid reference '{ref_path}': references cannot contain ASCII control characters")

            one_before_previous = previous
            previous = c

        if previous == ".":
            raise ValueError(f"Invalid reference '{ref_path}': references cannot end with a period (.)")
        elif previous == "/":
            raise ValueError(f"Invalid reference '{ref_path}': references cannot end with a forward slash (/)")
        elif previous == "@" and one_before_previous is None:
            raise ValueError(f"Invalid reference '{ref_path}': references cannot be '@'")
        elif any([component.endswith(".lock") for component in str(ref_path).split("/")]):
            raise ValueError(
                f"Invalid reference '{ref_path}': references cannot have slash-separated components that end with"
                f" '.lock'"
            )