Root cause
AI change
fix: validate split short-option values
Loading…
How AI contributed
Direct introductionThe check_unsafe_options guard can be bypassed on every guarded method (clone/clone_from, fetch/pull/push, ls_remote, iter_commits, blame, archive) by combining a single-character kwarg with split_single_char_options=False. The guard's candidate list omits the smuggled option, but transform_kwarg emits a JOINED -n<value> argv token that git parses as --upload-pack=<cmd>, yielding arbitrary command execution at the default allow_unsafe_options=False. This is an incomplete-fix bypass of commit ...
Root cause
fix: validate split short-option values
Fix
Check joined short-option values before Git execution
Code comparison
@@ -1039,11 +1039,18 @@ def _option_candidates(cls, args: Sequence[Any] =, kwargs: Optional[Mapping[s option for option in cls._unpack_args([arg for arg in args if arg is not None]) if option.startswith("-") ] if kwargs:+ split_single_char_options = kwargs.get("split_single_char_options", True) for key, value in kwargs.items(): values = value if isinstance(value, (list, tuple)) else (value,) if any(value is True or (value is not False and value is not None) for value in values): key = str(key) options.append(f"-{key}" if len(key) == 1 else f"--{dashify(key)}")+ if len(key) == 1 and split_single_char_options:+ options.extend(+ str(value)+ for value in values+ if value is not True and value not in (False, None) and str(value).startswith("-")+ ) return options AutoInterrupt: TypeAlias = _AutoInterruptGitPython: Unsafe git option guard bypass via split_single_char_options=False short-option token smuggling enables command execution
@@ -214,6 +214,23 @@ def test_option_candidates_ignore_untransformed_kwargs(self): self.assertEqual(options, ["--max-count"]) + def test_option_candidates_include_split_single_char_option_values(self):+ cases = [+ ({"n": "--upload-pack=helper"}, ["-n", "--upload-pack=helper"], ["--upload-pack"]),+ ({"g": ("safe", "--out=target")}, ["-g", "--out=target"], ["--output"]),+ ]++ for kwargs, candidates, unsafe_options in cases:+ self.assertEqual(Git._option_candidates(kwargs=kwargs), candidates)+ with self.assertRaises(UnsafeOptionError):+ Git.check_unsafe_options(options=candidates, unsafe_options=unsafe_options)++ self.assertEqual(self.git.transform_kwargs(n="--upload-pack=helper"), ["-n", "--upload-pack=helper"])++ unsplit_kwargs = {"n": "--upload-pack=helper", "split_single_char_options": False}+ self.assertEqual(self.git.transform_kwargs(**unsplit_kwargs), ["-n--upload-pack=helper"])+ self.assertEqual(Git._option_candidates(kwargs=unsplit_kwargs), ["-n"])+ _shell_cases = ( # value_in_call, value_from_class, expected_popen_arg (None, False, False),GitPython: Unsafe git option guard bypass via split_single_char_options=False short-option token smuggling enables command execution
@@ -1044,13 +1044,21 @@ def _option_candidates(cls, args: Sequence[Any] =, kwargs: Optional[Mapping[s values = value if isinstance(value, (list, tuple)) else (value,) if any(value is True or (value is not False and value is not None) for value in values): key = str(key)- options.append(f"-{key}" if len(key) == 1 else f"--{dashify(key)}")- if len(key) == 1 and split_single_char_options:+ if len(key) != 1:+ options.append(f"--{dashify(key)}")+ elif split_single_char_options:+ options.append(f"-{key}") options.extend( str(value) for value in values if value is not True and value not in (False, None) and str(value).startswith("-") )+ else:+ options.extend(+ f"-{key}" if value is True else f"-{key}{value}"+ for value in values+ if value is True or (value is not False and value is not None)+ ) return options AutoInterrupt: TypeAlias = _AutoInterruptGitPython: Unsafe git option guard bypass via split_single_char_options=False short-option token smuggling enables command execution
@@ -230,7 +230,15 @@ def test_option_candidates_include_split_single_char_option_values(self): unsplit_kwargs = {"n": "--upload-pack=helper", "split_single_char_options": False} self.assertEqual(self.git.transform_kwargs(**unsplit_kwargs), ["-n--upload-pack=helper"])- self.assertEqual(Git._option_candidates(kwargs=unsplit_kwargs), ["-n"])+ self.assertEqual(Git._option_candidates(kwargs=unsplit_kwargs), ["-n--upload-pack=helper"])++ def test_option_candidates_include_joined_single_char_option_values(self):+ kwargs = {"n": "uhelper", "split_single_char_options": False}+ candidates = Git._option_candidates(kwargs=kwargs)++ self.assertEqual(candidates, ["-nuhelper"])+ with self.assertRaises(UnsafeOptionError):+ Git.check_unsafe_options(options=candidates, unsafe_options=["-u"]) _shell_cases = ( # value_in_call, value_from_class, expected_popen_argGitPython: Unsafe git option guard bypass via split_single_char_options=False short-option token smuggling enables command execution
Candidate 3f95d3563cdce315c4cafde343db5289b955dfa2deb7a21c85a5a6927b24e47b · Fix d1ba267124cdd3725655b996f15366da87de66fbfa9bca61ce917f70d13f57d8
Releases
Advisory references