Skip to content

Commit 44ebbe2

Browse files
codexByron
authored andcommitted
Check joined short-option values before Git execution
GHSA-wvpp-8hx9-p66j reports that unsafe-option checks omitted the value joined to a one-character option when split_single_char_options was false. A regression test reproduced the mismatch: GitPython checked only -n even though it emitted a joined -nVALUE token that Git parses as clustered short options. Collect the exact joined token for unsplit one-character keyword arguments so the existing clustered-short-option validation sees every option character. The split form and long-option behavior remain unchanged. A broader audit confirmed that all guarded keyword-forwarding APIs use _option_candidates, including clone, ls-remote, fetch, pull, push, archive, revision, diff, checkout-index, and tag paths. Git cf5497b14 confirms repeated short-option parsing within a joined token. Focused candidate and unsafe-option tests pass.
1 parent 2589bc3 commit 44ebbe2

2 files changed

Lines changed: 19 additions & 3 deletions

File tree

git/cmd.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,13 +1044,21 @@ def _option_candidates(cls, args: Sequence[Any] = (), kwargs: Optional[Mapping[s
10441044
values = value if isinstance(value, (list, tuple)) else (value,)
10451045
if any(value is True or (value is not False and value is not None) for value in values):
10461046
key = str(key)
1047-
options.append(f"-{key}" if len(key) == 1 else f"--{dashify(key)}")
1048-
if len(key) == 1 and split_single_char_options:
1047+
if len(key) != 1:
1048+
options.append(f"--{dashify(key)}")
1049+
elif split_single_char_options:
1050+
options.append(f"-{key}")
10491051
options.extend(
10501052
str(value)
10511053
for value in values
10521054
if value is not True and value not in (False, None) and str(value).startswith("-")
10531055
)
1056+
else:
1057+
options.extend(
1058+
f"-{key}" if value is True else f"-{key}{value}"
1059+
for value in values
1060+
if value is True or (value is not False and value is not None)
1061+
)
10541062
return options
10551063

10561064
AutoInterrupt: TypeAlias = _AutoInterrupt

test/test_git.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,15 @@ def test_option_candidates_include_split_single_char_option_values(self):
230230

231231
unsplit_kwargs = {"n": "--upload-pack=helper", "split_single_char_options": False}
232232
self.assertEqual(self.git.transform_kwargs(**unsplit_kwargs), ["-n--upload-pack=helper"])
233-
self.assertEqual(Git._option_candidates(kwargs=unsplit_kwargs), ["-n"])
233+
self.assertEqual(Git._option_candidates(kwargs=unsplit_kwargs), ["-n--upload-pack=helper"])
234+
235+
def test_option_candidates_include_joined_single_char_option_values(self):
236+
kwargs = {"n": "uhelper", "split_single_char_options": False}
237+
candidates = Git._option_candidates(kwargs=kwargs)
238+
239+
self.assertEqual(candidates, ["-nuhelper"])
240+
with self.assertRaises(UnsafeOptionError):
241+
Git.check_unsafe_options(options=candidates, unsafe_options=["-u"])
234242

235243
_shell_cases = (
236244
# value_in_call, value_from_class, expected_popen_arg

0 commit comments

Comments
 (0)