From 142195888e713542189533a52cdfc333f05c3af6 Mon Sep 17 00:00:00 2001
From: w <w@mac.lan>
Date: Mon, 20 Apr 2026 23:29:50 -0400
Subject: [PATCH 1/3] Block unsafe underscored git kwargs / Fix for
 GHSA-rpm5-65cw-6hj4

---
 git/cmd.py          | 21 +++++++++++++--------
 test/test_clone.py  |  2 ++
 test/test_git.py    | 16 ++++++++++++++++
 test/test_remote.py |  5 +++--
 4 files changed, 34 insertions(+), 10 deletions(-)

Index: GitPython-3.1.44/git/cmd.py
===================================================================
--- GitPython-3.1.44.orig/git/cmd.py
+++ GitPython-3.1.44/git/cmd.py
@@ -712,21 +712,33 @@ class Git(metaclass=_GitMeta):
             )
 
     @classmethod
+    def _canonicalize_option_name(cls, option: str) -> str:
+        """Return the option name used for unsafe-option checks.
+
+        Examples:
+            ``"--upload-pack=/tmp/helper"`` -> ``"upload-pack"``
+            ``"upload_pack"`` -> ``"upload-pack"``
+            ``"--config core.filemode=false"`` -> ``"config"``
+        """
+        option_name = option.lstrip("-").split("=", 1)[0]
+        option_tokens = option_name.split(None, 1)
+        if not option_tokens:
+            return ""
+        return dashify(option_tokens[0])
+
+    @classmethod
     def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None:
         """Check for unsafe options.
 
         Some options that are passed to ``git <command>`` can be used to execute
         arbitrary commands. These are blocked by default.
         """
-        # Options can be of the form `foo`, `--foo bar`, or `--foo=bar`, so we need to
-        # check if they start with "--foo" or if they are equal to "foo".
-        bare_unsafe_options = [option.lstrip("-") for option in unsafe_options]
+        # Options can be of the form `foo`, `--foo`, `--foo bar`, or `--foo=bar`.
+        canonical_unsafe_options = {cls._canonicalize_option_name(option): option for option in unsafe_options}
         for option in options:
-            for unsafe_option, bare_option in zip(unsafe_options, bare_unsafe_options):
-                if option.startswith(unsafe_option) or option == bare_option:
-                    raise UnsafeOptionError(
-                        f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
-                    )
+            unsafe_option = canonical_unsafe_options.get(cls._canonicalize_option_name(option))
+            if unsafe_option is not None:
+                raise UnsafeOptionError(f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it.")
 
     class AutoInterrupt:
         """Process wrapper that terminates the wrapped process on finalization.
Index: GitPython-3.1.44/test/test_git.py
===================================================================
--- GitPython-3.1.44.orig/test/test_git.py
+++ GitPython-3.1.44/test/test_git.py
@@ -26,6 +26,7 @@ else:
 import ddt
 
 from git import Git, GitCommandError, GitCommandNotFound, Repo, cmd, refresh
+from git.exc import UnsafeOptionError
 from git.util import cwd, finalize_process
 
 from test.lib import TestBase, fixture_path, with_rw_directory
@@ -153,6 +154,21 @@ class TestGit(TestBase):
         res = self.git.transform_kwargs(**{"s": True, "t": True})
         self.assertEqual({"-s", "-t"}, set(res))
 
+    def test_check_unsafe_options_normalizes_kwargs(self):
+        cases = [
+            (["upload_pack"], ["--upload-pack"]),
+            (["receive_pack"], ["--receive-pack"]),
+            (["exec"], ["--exec"]),
+            (["u"], ["-u"]),
+            (["c"], ["-c"]),
+            (["--upload-pack=/tmp/helper"], ["--upload-pack"]),
+            (["--config core.filemode=false"], ["--config"]),
+        ]
+
+        for options, unsafe_options in cases:
+            with self.assertRaises(UnsafeOptionError):
+                Git.check_unsafe_options(options=options, unsafe_options=unsafe_options)
+
     _shell_cases = (
         # value_in_call, value_from_class, expected_popen_arg
         (None, False, False),
Index: GitPython-3.1.44/test/test_remote.py
===================================================================
--- GitPython-3.1.44.orig/test/test_remote.py
+++ GitPython-3.1.44/test/test_remote.py
@@ -824,7 +824,7 @@ class TestRemote(TestBase):
             remote = rw_repo.remote("origin")
             tmp_dir = Path(tdir)
             tmp_file = tmp_dir / "pwn"
-            unsafe_options = [{"upload-pack": f"touch {tmp_file}"}]
+            unsafe_options = [{"upload-pack": f"touch {tmp_file}"}, {"upload_pack": f"touch {tmp_file}"}]
             for unsafe_option in unsafe_options:
                 with self.assertRaises(UnsafeOptionError):
                     remote.fetch(**unsafe_option)
@@ -892,7 +892,7 @@ class TestRemote(TestBase):
             remote = rw_repo.remote("origin")
             tmp_dir = Path(tdir)
             tmp_file = tmp_dir / "pwn"
-            unsafe_options = [{"upload-pack": f"touch {tmp_file}"}]
+            unsafe_options = [{"upload-pack": f"touch {tmp_file}"}, {"upload_pack": f"touch {tmp_file}"}]
             for unsafe_option in unsafe_options:
                 with self.assertRaises(UnsafeOptionError):
                     remote.pull(**unsafe_option)
@@ -961,10 +961,9 @@ class TestRemote(TestBase):
             tmp_dir = Path(tdir)
             tmp_file = tmp_dir / "pwn"
             unsafe_options = [
-                {
-                    "receive-pack": f"touch {tmp_file}",
-                    "exec": f"touch {tmp_file}",
-                }
+                {"receive-pack": f"touch {tmp_file}"},
+                {"receive_pack": f"touch {tmp_file}"},
+                {"exec": f"touch {tmp_file}"},
             ]
             for unsafe_option in unsafe_options:
                 assert not tmp_file.exists()
@@ -988,10 +987,9 @@ class TestRemote(TestBase):
             tmp_dir = Path(tdir)
             tmp_file = tmp_dir / "pwn"
             unsafe_options = [
-                {
-                    "receive-pack": f"touch {tmp_file}",
-                    "exec": f"touch {tmp_file}",
-                }
+                {"receive-pack": f"touch {tmp_file}"},
+                {"receive_pack": f"touch {tmp_file}"},
+                {"exec": f"touch {tmp_file}"},
             ]
             for unsafe_option in unsafe_options:
                 # The options will be allowed, but the command will fail.
