Root cause
Injection & unsafe execution
fix: Guard unsafe git command options (GHSA-956x-8gvw-wg5v)
How AI contributed: Flawed AI-written codePublished Aug 10, 2026 in
gitpython-developers/gitpythonAll 8 findings in this repository
Repo.blame() and Repo.blame_incremental() collect a caller-controlled rev as an option candidate and then pass that value to git blame before the '--' path separator. The guard introduced for unsafe revision options listed only the write options --output/-o, so --contents=<path> and -S=<path> passed with allow_unsafe_options=False. Git blame treats those as file-input options, and its porcelain result exposes victim-file lines to the API caller.
The guard checked a short denylist before passing the caller-controlled revision to git blame. File-input options remained accepted before the path separator, so Git read the chosen victim file and returned its contents through blame output.
Root cause
fix: Guard unsafe git command options (GHSA-956x-8gvw-wg5v)
Fix
The fix extends blame validation to reject the file-reading --contents, -S, and --ignore-revs-file options before the caller-controlled revision reaches git blame, while retaining the explicit unsafe-options opt-out.
Case facts
@@ -1071,7 +1071,10 @@ def fetch( Git.check_unsafe_protocols(ref) if not allow_unsafe_options:- Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_fetch_options)+ Git.check_unsafe_options(+ options=Git._option_candidates([], kwargs),+ unsafe_options=self.unsafe_git_fetch_options,+ ) proc = self.repo.git.fetch( "--", self, *args, as_process=True, with_stdout=False, universal_newlines=True, v=verbose, **kwargs@@ -1125,7 +1128,10 @@ def pull( Git.check_unsafe_protocols(ref) if not allow_unsafe_options:- Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_pull_options)+ Git.check_unsafe_options(+ options=Git._option_candidates([], kwargs),+ unsafe_options=self.unsafe_git_pull_options,+ ) proc = self.repo.git.pull( "--", self, refspec, with_stdout=False, as_process=True, universal_newlines=True, v=True, **kwargs@@ -1198,7 +1204,10 @@ def push( Git.check_unsafe_protocols(ref) if not allow_unsafe_options:- Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_push_options)+ Git.check_unsafe_options(+ options=Git._option_candidates([], kwargs),+ unsafe_options=self.unsafe_git_push_options,+ ) proc = self.repo.git.push( "--",@@ -161,6 +161,20 @@ class Repo: https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---configltkeygtltvaluegt """ + unsafe_git_archive_options = [+ # Allows arbitrary command execution through the remote git-upload-archive command.+ "--exec",+ # Writes output to a caller-controlled filesystem path.+ "--output",+ "-o",+ ]++ unsafe_git_revision_options = [+ # This option allows output to be written to arbitrary files before revision parsing.+ "--output",+ "-o",+ ]+ # Invariants config_level: ConfigLevels_Tup = ("system", "user", "global", "repository") """Represents the configuration level of a configuration file."""@@ -775,6 +789,7 @@ def iter_commits( self, rev: Union[str, Commit, "SymbolicReference", None] = None, paths: Union[PathLike, Sequence[PathLike]] = "",+ allow_unsafe_options: bool = False, **kwargs: Any, ) -> Iterator[Commit]: """An iterator of :class:`~git.objects.commit.Commit` objects representing the@@ -792,6 +807,9 @@ def iter_commits( Arguments to be passed to :manpage:`git-rev-list(1)`. Common ones are ``max_count`` and ``skip``. + :param allow_unsafe_options:+ Allow unsafe options in the revision argument, like ``--output``.+ :note: To receive only commits between two named revisions, use the ``"revA...revB"`` revision specifier.@@ -802,7 +820,18 @@ def iter_commits( if rev is None: rev = self.head.commit - return Commit.iter_items(self, rev, paths, **kwargs)+ if not allow_unsafe_options:+ Git.check_unsafe_options(+ options=Git._option_candidates([rev], kwargs), unsafe_options=self.unsafe_git_revision_options+ )++ return Commit.iter_items(+ self,+ rev,+ paths,+ allow_unsafe_options=allow_unsafe_options,+ **kwargs,+ ) def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Commit]: R"""Find the closest common ancestor for the given revision@@ -1079,7 +1108,9 @@ def active_branch(self) -> Head: ) return active_branch - def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) -> Iterator["BlameEntry"]:+ def blame_incremental(+ self, rev: str | HEAD | None, file: str, allow_unsafe_options: bool = False, **kwargs: Any+ ) -> Iterator["BlameEntry"]: """Iterator for blame information for the given file at the given revision. Unlike :meth:`blame`, this does not return the actual file's contents, only a@@ -1090,6 +1121,9 @@ def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) -> uncommitted changes. Otherwise, anything successfully parsed by :manpage:`git-rev-parse(1)` is a valid option. + :param allow_unsafe_options:+ Allow unsafe options in revision argument, like ``--output``.+ :return: Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates the commit to blame for the line, and range indicates a span of line numbers in@@ -1098,6 +1132,10 @@ def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) -> If you combine all line number ranges outputted by this command, you should get a continuous range spanning all line numbers in the file. """+ if not allow_unsafe_options:+ Git.check_unsafe_options(+ options=Git._option_candidates([rev], kwargs), unsafe_options=self.unsafe_git_revision_options+ ) data: bytes = self.git.blame(rev, "--", file, p=True, incremental=True, stdout_as_string=False, **kwargs)@@ -86,6 +86,12 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable): # INVARIANTS default_encoding = "UTF-8" + # Options to :manpage:`git-rev-list(1)` that can overwrite files.+ unsafe_git_rev_options = [+ "--output",+ "-o",+ ]+ type: Literal["commit"] = "commit" __slots__ = (@@ -302,6 +308,7 @@ def iter_items( repo: "Repo", rev: Union[str, "Commit", "SymbolicReference"], paths: Union[PathLike, Sequence[PathLike]] = "",+ allow_unsafe_options: bool = False, **kwargs: Any, ) -> Iterator["Commit"]: R"""Find all commits matching the given criteria.@@ -330,6 +337,11 @@ def iter_items( raise ValueError("--pretty cannot be used as parsing expects single sha's only") # END handle pretty + if not allow_unsafe_options:+ Git.check_unsafe_options(+ options=Git._option_candidates([rev], kwargs), unsafe_options=cls.unsafe_git_rev_options+ )+ # Use -- in all cases, to prevent possibility of ambiguous arguments. # See https://github.com/gitpython-developers/GitPython/issues/264. @@ -199,6 +199,19 @@ class Repo: "-o", ] + unsafe_git_blame_options = unsafe_git_revision_options + [+ # These options read from arbitrary files and expose their contents through blame output.+ "--contents",+ "-S",+ "--ignore-revs-file",+ ]++ unsafe_git_diff_options = unsafe_git_revision_options + [+ # Reads caller-controlled order patterns from an arbitrary file.+ "-O",+ "--orderfile",+ ]+ # Invariants config_level: ConfigLevels_Tup = ("system", "user", "global", "repository") """Represents the configuration level of a configuration file."""@@ -1149,7 +1162,7 @@ def blame_incremental( :manpage:`git-rev-parse(1)` is a valid option. :param allow_unsafe_options:- Allow unsafe options in revision argument, like ``--output``.+ Allow unsafe options in revision argument, like ``--output`` or ``--contents``. :return: Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates the@@ -1161,7 +1174,9 @@ def blame_incremental( """ if not allow_unsafe_options: Git.check_unsafe_options(- options=Git._option_candidates([rev], kwargs), unsafe_options=self.unsafe_git_revision_options+ options=Git._option_candidates([rev], kwargs),+ unsafe_options=self.unsafe_git_blame_options,+ clusterable_short_options="46bceflnpqstvw", ) data: bytes = self.git.blame(rev, "--", file, p=True, incremental=True, stdout_as_string=False, **kwargs)@@ -1253,7 +1268,7 @@ def blame( :manpage:`git-rev-parse(1)` is a valid option. :param allow_unsafe_options:- Allow unsafe options in revision argument, like ``--output``.+ Allow unsafe options in revision argument, like ``--output`` or ``--contents``. :return: list: [git.Commit, list: [<line>]]@@ -1269,7 +1284,8 @@ def blame( if not allow_unsafe_options: Git.check_unsafe_options( options=Git._option_candidates([rev, rev_opts_list], kwargs),- unsafe_options=self.unsafe_git_revision_options,+ unsafe_options=self.unsafe_git_blame_options,+ clusterable_short_options="46bceflnpqstvw", ) data: bytes = self.git.blame(rev, *rev_opts_list, "--", file, p=True, stdout_as_string=False, **kwargs) commits: Dict[str, Commit] = {}AI-assisted change 74f7c407112b8bcc7c96f8d9c7329f9eb0d3e2e809d687bbba7ce45b528d3d79 · Fix 07295a54024a4c5dfe8b681500e550694d23dd6e07fed43542e1920b7a22de0c