Skip to content

Commit 1421958

Browse files
committed
Block unsafe underscored git kwargs / Fix for GHSA-rpm5-65cw-6hj4
1 parent da54523 commit 1421958

4 files changed

Lines changed: 34 additions & 10 deletions

File tree

git/cmd.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -944,22 +944,27 @@ def check_unsafe_protocols(cls, url: str) -> None:
944944
f"The `{protocol}::` protocol looks suspicious, use `allow_unsafe_protocols=True` to allow it."
945945
)
946946

947+
@classmethod
948+
def _canonicalize_option_name(cls, option: str) -> str:
949+
"""Normalize an option or kwarg name for unsafe-option checks."""
950+
option_name = option.lstrip("-").split("=", 1)[0].split(None, 1)[0]
951+
return dashify(option_name)
952+
947953
@classmethod
948954
def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None:
949955
"""Check for unsafe options.
950956
951957
Some options that are passed to ``git <command>`` can be used to execute
952958
arbitrary commands. These are blocked by default.
953959
"""
954-
# Options can be of the form `foo`, `--foo bar`, or `--foo=bar`, so we need to
955-
# check if they start with "--foo" or if they are equal to "foo".
956-
bare_unsafe_options = [option.lstrip("-") for option in unsafe_options]
960+
# Options can be of the form `foo`, `--foo`, `--foo bar`, or `--foo=bar`.
961+
canonical_unsafe_options = {cls._canonicalize_option_name(option): option for option in unsafe_options}
957962
for option in options:
958-
for unsafe_option, bare_option in zip(unsafe_options, bare_unsafe_options):
959-
if option.startswith(unsafe_option) or option == bare_option:
960-
raise UnsafeOptionError(
961-
f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
962-
)
963+
unsafe_option = canonical_unsafe_options.get(cls._canonicalize_option_name(option))
964+
if unsafe_option is not None:
965+
raise UnsafeOptionError(
966+
f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
967+
)
963968

964969
AutoInterrupt: TypeAlias = _AutoInterrupt
965970

test/test_clone.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ def test_clone_unsafe_options(self, rw_repo):
128128

129129
unsafe_options = [
130130
{"upload-pack": f"touch {tmp_file}"},
131+
{"upload_pack": f"touch {tmp_file}"},
131132
{"u": f"touch {tmp_file}"},
132133
{"config": "protocol.ext.allow=always"},
133134
{"c": "protocol.ext.allow=always"},
@@ -216,6 +217,7 @@ def test_clone_from_unsafe_options(self, rw_repo):
216217

217218
unsafe_options = [
218219
{"upload-pack": f"touch {tmp_file}"},
220+
{"upload_pack": f"touch {tmp_file}"},
219221
{"u": f"touch {tmp_file}"},
220222
{"config": "protocol.ext.allow=always"},
221223
{"c": "protocol.ext.allow=always"},

test/test_git.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import ddt
2828

2929
from git import Git, GitCommandError, GitCommandNotFound, Repo, cmd, refresh
30+
from git.exc import UnsafeOptionError
3031
from git.util import cwd, finalize_process
3132

3233
from test.lib import TestBase, fixture_path, with_rw_directory
@@ -154,6 +155,21 @@ def test_it_transforms_kwargs_into_git_command_arguments(self):
154155
res = self.git.transform_kwargs(**{"s": True, "t": True})
155156
self.assertEqual({"-s", "-t"}, set(res))
156157

158+
def test_check_unsafe_options_normalizes_kwargs(self):
159+
cases = [
160+
(["upload_pack"], ["--upload-pack"]),
161+
(["receive_pack"], ["--receive-pack"]),
162+
(["exec"], ["--exec"]),
163+
(["u"], ["-u"]),
164+
(["c"], ["-c"]),
165+
(["--upload-pack=/tmp/helper"], ["--upload-pack"]),
166+
(["--config core.filemode=false"], ["--config"]),
167+
]
168+
169+
for options, unsafe_options in cases:
170+
with self.assertRaises(UnsafeOptionError):
171+
Git.check_unsafe_options(options=options, unsafe_options=unsafe_options)
172+
157173
_shell_cases = (
158174
# value_in_call, value_from_class, expected_popen_arg
159175
(None, False, False),

test/test_remote.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,7 @@ def test_fetch_unsafe_options(self, rw_repo):
827827
remote = rw_repo.remote("origin")
828828
tmp_dir = Path(tdir)
829829
tmp_file = tmp_dir / "pwn"
830-
unsafe_options = [{"upload-pack": f"touch {tmp_file}"}]
830+
unsafe_options = [{"upload-pack": f"touch {tmp_file}"}, {"upload_pack": f"touch {tmp_file}"}]
831831
for unsafe_option in unsafe_options:
832832
with self.assertRaises(UnsafeOptionError):
833833
remote.fetch(**unsafe_option)
@@ -895,7 +895,7 @@ def test_pull_unsafe_options(self, rw_repo):
895895
remote = rw_repo.remote("origin")
896896
tmp_dir = Path(tdir)
897897
tmp_file = tmp_dir / "pwn"
898-
unsafe_options = [{"upload-pack": f"touch {tmp_file}"}]
898+
unsafe_options = [{"upload-pack": f"touch {tmp_file}"}, {"upload_pack": f"touch {tmp_file}"}]
899899
for unsafe_option in unsafe_options:
900900
with self.assertRaises(UnsafeOptionError):
901901
remote.pull(**unsafe_option)
@@ -966,6 +966,7 @@ def test_push_unsafe_options(self, rw_repo):
966966
unsafe_options = [
967967
{
968968
"receive-pack": f"touch {tmp_file}",
969+
"receive_pack": f"touch {tmp_file}",
969970
"exec": f"touch {tmp_file}",
970971
}
971972
]

0 commit comments

Comments
 (0)