From fcd8d016816696780c0dc96dacbe48fc10df80f6 Mon Sep 17 00:00:00 2001 From: "GPT 5.5" Date: Tue, 28 Apr 2026 09:17:31 +0800 Subject: [PATCH] prevent out-of-repo access when manipulating references. This previously made it possible to create, modify and delete files outside outside of the repository, which is a problem if inputs aren't trusted. CVE: CVE-2026-44243 Upstream-Status: Backport [https://github.com/gitpython-developers/GitPython/commit/25ba54dd3fb374b8fade7de4be1ac2ac84722190] Backport Changes: - Omit regression tests because the Scarthgap PyPI source archive does not include the upstream test suite. Co-authored-by: Sebastian Thiel (cherry picked from commit 25ba54dd3fb374b8fade7de4be1ac2ac84722190) Signed-off-by: Darsh Kelaiya --- git/refs/log.py | 2 +- git/refs/remote.py | 5 +++-- git/refs/symbolic.py | 37 +++++++++++++++++++++++++++++++------ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/git/refs/log.py b/git/refs/log.py index e45798d8..29293f4a 100644 --- a/git/refs/log.py +++ b/git/refs/log.py @@ -204,7 +204,7 @@ class RefLog(List[RefLogEntry], Serializable): file though. :param ref: SymbolicReference instance """ - return osp.join(ref.repo.git_dir, "logs", to_native_path(ref.path)) + return to_native_path(ref._get_validated_reflog_path(ref.repo, ref.path)) @classmethod def iter_entries(cls, stream: Union[str, "BytesIO", mmap]) -> Iterator[RefLogEntry]: diff --git a/git/refs/remote.py b/git/refs/remote.py index 59d02a75..e50c54eb 100644 --- a/git/refs/remote.py +++ b/git/refs/remote.py @@ -64,12 +64,13 @@ class RemoteReference(Head): # are generally ignored in the refs/ folder. We don't though # and delete remainders manually. for ref in refs: + cls._check_ref_name_valid(ref.path) try: - os.remove(os.path.join(repo.common_dir, ref.path)) + os.remove(cls._get_validated_path(repo.common_dir, ref.path)) except OSError: pass try: - os.remove(os.path.join(repo.git_dir, ref.path)) + os.remove(cls._get_validated_path(repo.git_dir, ref.path)) except OSError: pass # END for each ref diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 31f959ac..d5c18290 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -109,6 +109,32 @@ class SymbolicReference: def abspath(self) -> PathLike: return join_path_native(_git_dir(self.repo, self.path), self.path) + @staticmethod + def _get_validated_path(base: PathLike, path: PathLike) -> str: + path = os.fspath(path) + base_path = os.path.realpath(os.fspath(base)) + abs_path = os.path.realpath(os.path.join(base_path, path)) + try: + common_path = os.path.commonpath([base_path, abs_path]) + except ValueError as e: + raise ValueError("Reference path %r escapes the repository" % path) from e + if os.path.normcase(common_path) != os.path.normcase(base_path): + raise ValueError("Reference path %r escapes the repository" % path) + return abs_path + + @classmethod + def _get_validated_ref_path(cls, repo: "Repo", path: PathLike) -> str: + """Return the absolute filesystem path for a ref after validating it.""" + cls._check_ref_name_valid(path) + ref_path = os.fspath(path) + return cls._get_validated_path(_git_dir(repo, ref_path), ref_path) + + @classmethod + def _get_validated_reflog_path(cls, repo: "Repo", path: PathLike) -> str: + """Return the absolute filesystem path for a reflog after validating it.""" + cls._check_ref_name_valid(path) + return cls._get_validated_path(os.path.join(repo.git_dir, "logs"), path) + @classmethod def _get_packed_refs_path(cls, repo: "Repo") -> str: return os.path.join(repo.common_dir, "packed-refs") @@ -442,7 +468,7 @@ class SymbolicReference: # END handle non-existing # END retrieve old hexsha - fpath = self.abspath + fpath = self._get_validated_ref_path(self.repo, self.path) assure_directory_exists(fpath, is_file=True) lfd = LockedFD(fpath) @@ -571,7 +597,7 @@ class SymbolicReference: Alternatively the symbolic reference to be deleted. """ full_ref_path = cls.to_full_path(path) - abs_path = os.path.join(repo.common_dir, full_ref_path) + abs_path = cls._get_validated_ref_path(repo, full_ref_path) if os.path.exists(abs_path): os.remove(abs_path) else: @@ -635,9 +661,8 @@ class SymbolicReference: corresponding object and a detached symbolic reference will be created instead. """ - git_dir = _git_dir(repo, path) full_ref_path = cls.to_full_path(path) - abs_ref_path = os.path.join(git_dir, full_ref_path) + abs_ref_path = cls._get_validated_ref_path(repo, full_ref_path) # Figure out target data. target = reference @@ -724,8 +749,8 @@ class SymbolicReference: if self.path == new_path: return self - new_abs_path = os.path.join(_git_dir(self.repo, new_path), new_path) - cur_abs_path = os.path.join(_git_dir(self.repo, self.path), self.path) + new_abs_path = self._get_validated_ref_path(self.repo, new_path) + cur_abs_path = self._get_validated_ref_path(self.repo, self.path) if os.path.isfile(new_abs_path): if not force: # If they point to the same file, it's not an error. -- 2.35.6