new file mode 100644
@@ -0,0 +1,346 @@
+From be9e81b3ded21bb7e20d9d679cf3409002a7bc79 Mon Sep 17 00:00:00 2001
+From: "GPT 5.6" <codex@openai.com>
+Date: Sun, 12 Jul 2026 15:06:42 +0200
+Subject: [PATCH 11/38] fix: Guard unsafe git command options
+ (GHSA-956x-8gvw-wg5v)
+
+Reject command options that can execute arbitrary helpers or write files when passed through archive, revision traversal, blame, clone, fetch, pull, push, and ls-remote APIs. Validate positional option tokens and transformed keyword options consistently, including Git-supported long-option abbreviations, attached short-option values, and falsey stringifiable remotes. Preserve explicit allow_unsafe_options and allow_unsafe_protocols escape hatches.
+
+Add regression coverage for each guarded API, option spelling and abbreviation forms, inert keyword values, mixed positional and keyword candidates, tuple blame options, stringifiable remotes, and unsafe commands that must not create marker files.
+
+CVE: CVE-2026-67323
+Upstream-Status: Backport [https://github.com/gitpython-developers/GitPython/commit/701ce32fe5ba8cb622c0e0342a376a6beb47d738]
+
+Backport Changes:
+- Omit upstream regression test changes because the current
+ PyPI source archive does not include its test suite.
+- Port the guards into the 3.1.42 method signatures where
+ the upstream API had since changed.
+
+Reviewed-by: Sebastian Thiel <sebastian.thiel@icloud.com>
+(cherry picked from commit 701ce32fe5ba8cb622c0e0342a376a6beb47d738)
+Signed-off-by: Yogita Urade <yurade@cisco.com>
+---
+ git/cmd.py | 22 +++++++++++-
+ git/objects/commit.py | 12 ++++++
+ git/remote.py | 15 ++++++--
+ git/repo/base.py | 93 +++++++++++++++++++++++++++++++++++++++++++------
+ 4 files changed, 126 insertions(+), 16 deletions(-)
+
+diff --git a/git/cmd.py b/git/cmd.py
+index 8e8cd0c5..81ea0b3c 100644
+--- a/git/cmd.py
++++ b/git/cmd.py
+@@ -316,6 +316,11 @@ class Git(LazyMixin):
+
+ re_unsafe_protocol = re.compile(r"(.+)::.+")
+
++ unsafe_git_ls_remote_options = [
++ # This option allows arbitrary command execution in git-ls-remote.
++ "--upload-pack",
++ ]
++
+ def __getstate__(self) -> Dict[str, Any]:
+ return slots_to_dict(self, exclude=self._excluded_)
+
+@@ -853,6 +858,21 @@ class Git(LazyMixin):
+ else:
+ super()._set_cache_(attr)
+ # END handle version info
++ def ls_remote(
++ self,
++ *args: Any,
++ allow_unsafe_options: bool = False,
++ **kwargs: Any,
++ ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]:
++ """List references in a remote repository.
++
++ :param allow_unsafe_options:
++ Allow unsafe options, like ``--upload-pack``.
++ """
++ if not allow_unsafe_options:
++ candidate_options = self._option_candidates(args, kwargs)
++ Git.check_unsafe_options(options=candidate_options, unsafe_options=self.unsafe_git_ls_remote_options)
++ return self._call_process("ls_remote", *args, **kwargs)
+
+ @property
+ def working_dir(self) -> Union[None, PathLike]:
+@@ -1295,7 +1315,7 @@ class Git(LazyMixin):
+ return args
+
+ @classmethod
+- def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]:
++ def _unpack_args(cls, arg_list: Sequence[Any]) -> List[str]:
+ outlist = []
+ if isinstance(arg_list, (list, tuple)):
+ for arg in arg_list:
+diff --git a/git/objects/commit.py b/git/objects/commit.py
+index caec1b6c..ae60d286 100644
+--- a/git/objects/commit.py
++++ b/git/objects/commit.py
+@@ -76,6 +76,12 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
+ default_encoding = "UTF-8"
+
+ # object configuration
++ # Options to :manpage:`git-rev-list(1)` that can overwrite files.
++ unsafe_git_rev_options = [
++ "--output",
++ "-o",
++ ]
++
+ type: Literal["commit"] = "commit"
+ __slots__ = (
+ "tree",
+@@ -269,6 +275,7 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
+ repo: "Repo",
+ rev: Union[str, "Commit", "SymbolicReference"], # type: ignore
+ paths: Union[PathLike, Sequence[PathLike]] = "",
++ allow_unsafe_options: bool = False,
+ **kwargs: Any,
+ ) -> Iterator["Commit"]:
+ """Find all commits matching the given criteria.
+@@ -294,6 +301,11 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
+ 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.
+
+diff --git a/git/remote.py b/git/remote.py
+index 3ccac59d..25e076f3 100644
+--- a/git/remote.py
++++ b/git/remote.py
+@@ -1005,7 +1005,10 @@ class Remote(LazyMixin, IterableObj):
+ 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
+@@ -1046,7 +1049,10 @@ class Remote(LazyMixin, IterableObj):
+ 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
+@@ -1111,7 +1117,10 @@ class Remote(LazyMixin, IterableObj):
+ 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(
+ "--",
+diff --git a/git/repo/base.py b/git/repo/base.py
+index 92ace3a0..5b255997 100644
+--- a/git/repo/base.py
++++ b/git/repo/base.py
+@@ -152,6 +152,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."""
+@@ -690,6 +704,7 @@ class Repo:
+ self,
+ rev: Union[str, Commit, "SymbolicReference", None] = None,
+ paths: Union[PathLike, Sequence[PathLike]] = "",
++ allow_unsafe_options: bool = False,
+ **kwargs: Any,
+ ) -> Iterator[Commit]:
+ """A list of Commit objects representing the history of a given ref/commit.
+@@ -705,7 +720,11 @@ class Repo:
+ :param kwargs:
+ Arguments to be passed to git-rev-list - common ones are max_count and skip.
+
+- :note: To receive only commits between two named revisions, use the
++ :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.
+
+ :return: ``git.Commit[]``
+@@ -713,7 +732,18 @@ class Repo:
+ 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[Union[Commit_ish, None]]:
+ """Find the closest common ancestor for the given revision (Commits, Tags, References, etc.).
+@@ -957,7 +987,9 @@ class Repo:
+ # reveal_type(self.head.reference) # => Reference
+ return self.head.reference
+
+- def blame_incremental(self, rev: str | HEAD, 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
+@@ -965,13 +997,21 @@ class Repo:
+
+ :param rev: Revision specifier, see git-rev-parse for viable options.
+
+- :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 the resulting file.
++ :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
++ the resulting file.
+
+ 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)
+ commits: Dict[bytes, Commit] = {}
+@@ -1048,13 +1088,17 @@ class Repo:
+ rev: Union[str, HEAD],
+ file: str,
+ incremental: bool = False,
+- rev_opts: Optional[List[str]] = None,
++ rev_opts: Optional[Sequence[str]] = None,
++ allow_unsafe_options: bool = False,
+ **kwargs: Any,
+ ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None:
+ """The blame information for the given file at the given revision.
+
+ :param rev: Revision specifier, see git-rev-parse for viable options.
+
++ :param allow_unsafe_options:
++ Allow unsafe options in revision argument, like ``--output``.
++
+ :return:
+ list: [git.Commit, list: [<line>]]
+
+@@ -1063,9 +1107,14 @@ class Repo:
+ of appearance.
+ """
+ if incremental:
+- return self.blame_incremental(rev, file, **kwargs)
+- rev_opts = rev_opts or []
+- data: bytes = self.git.blame(rev, *rev_opts, "--", file, p=True, stdout_as_string=False, **kwargs)
++ return self.blame_incremental(rev, file, allow_unsafe_options=allow_unsafe_options, **kwargs)
++ rev_opts_list = list(rev_opts or [])
++ 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,
++ )
++ data: bytes = self.git.blame(rev, *rev_opts_list, "--", file, p=True, stdout_as_string=False, **kwargs)
+ commits: Dict[str, Commit] = {}
+ blames: List[List[Commit | List[str | bytes] | None]] = []
+
+@@ -1270,7 +1319,10 @@ class Repo:
+ if not allow_unsafe_protocols:
+ Git.check_unsafe_protocols(str(url))
+ if not allow_unsafe_options:
+- Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=cls.unsafe_git_clone_options)
++ Git.check_unsafe_options(
++ options=Git._option_candidates([], kwargs),
++ unsafe_options=cls.unsafe_git_clone_options,
++ )
+ if not allow_unsafe_options and multi:
+ Git.check_unsafe_options(options=multi, unsafe_options=cls.unsafe_git_clone_options)
+
+@@ -1419,6 +1471,8 @@ class Repo:
+ ostream: Union[TextIO, BinaryIO],
+ treeish: Optional[str] = None,
+ prefix: Optional[str] = None,
++ allow_unsafe_options: bool = False,
++ allow_unsafe_protocols: bool = False,
+ **kwargs: Any,
+ ) -> Repo:
+ """Archive the tree at the given revision.
+@@ -1436,7 +1490,14 @@ class Repo:
+ * You may specify the special **path** keyword, which may either be a repository-relative
+ path to a directory or file to place into the archive, or a list or tuple of multiple paths.
+
+- :raise GitCommandError: If something went wrong.
++ :param allow_unsafe_options:
++ Allow unsafe options, like ``--exec`` or ``--output``.
++
++ :param allow_unsafe_protocols:
++ Allow unsafe protocols to be used in ``remote``, like ``ext``.
++
++ :raise git.exc.GitCommandError:
++ If something went wrong.
+
+ :return: self
+ """
+@@ -1444,6 +1505,14 @@ class Repo:
+ treeish = self.head.commit
+ if prefix and "prefix" not in kwargs:
+ kwargs["prefix"] = prefix
++ remote = kwargs.get("remote")
++ if not allow_unsafe_protocols and remote is not None:
++ Git.check_unsafe_protocols(str(remote))
++ if not allow_unsafe_options:
++ Git.check_unsafe_options(
++ options=Git._option_candidates([], kwargs),
++ unsafe_options=self.unsafe_git_archive_options,
++ )
+ kwargs["output_stream"] = ostream
+ path = kwargs.pop("path", [])
+ path = cast(Union[PathLike, List[PathLike], Tuple[PathLike, ...]], path)
+--
+2.35.6
@@ -22,6 +22,7 @@ SRC_URI += " file://CVE-2026-42284.patch \
file://CVE-2026-42215_p1.patch \
file://CVE-2026-42215_p2.patch \
file://CVE-2026-67326.patch \
+ file://CVE-2026-67323.patch \
"
SRC_URI[sha256sum] = "2d99869e0fef71a73cbd242528105af1d6c1b108c60dfabd994bf292f76c3ceb"
This patch applies the upstream fix as referenced in [2], using the commit shown in [1]. [1] https://github.com/gitpython-developers/GitPython/commit/701ce32fe5ba8cb622c0e0342a376a6beb47d738 [2] https://www.cve.org/CVERecord?id=CVE-2026-67323 Signed-off-by: Yogita Urade <yurade@cisco.com> --- .../python/python3-git/CVE-2026-67323.patch | 346 ++++++++++++++++++ .../python/python3-git_3.1.42.bb | 1 + 2 files changed, 347 insertions(+) create mode 100644 meta/recipes-devtools/python/python3-git/CVE-2026-67323.patch