@@ -961,17 +961,66 @@ def _canonicalize_option_name(cls, option: str) -> str:
961961
962962 @classmethod
963963 def check_unsafe_options (cls , options : List [str ], unsafe_options : List [str ]) -> None :
964- """Check for unsafe options.
965-
966- Some options that are passed to ``git <command>`` can be used to execute
967- arbitrary commands. These are blocked by default.
964+ """Raise :class:`~git.exc.UnsafeOptionError` for blocked option spellings.
965+
966+ In addition to exact matches, this rejects abbreviated long options accepted
967+ by Git (for example, ``--upl`` for ``--upload-pack``) and unsafe short options
968+ whose values are joined to the same token, including after clusterable flags
969+ (for example, ``-uVALUE`` and ``-fuVALUE``).
970+
971+ A list containing only bare names is treated as normalized keyword arguments,
972+ so multi-character names such as ``upload_p`` are checked as long-option
973+ abbreviations. If any item starts with ``-``, the list is treated as tokenized
974+ command-line input: bare items can be option values and are not checked as
975+ abbreviations. Thus ``["--origin", "upload"]`` is allowed. Single-dash options
976+ use short-option parsing rather than broad prefix matching, preserving safe
977+ attached values such as ``-oupstream`` and ``-bcurrent``.
978+
979+ Some options passed to ``git <command>`` can execute arbitrary commands and
980+ are therefore blocked by default unless the caller explicitly allows them.
968981 """
969982 # Options can be of the form `foo`, `--foo`, `--foo bar`, or `--foo=bar`.
983+ # Git accepts any unambiguous prefix of a long option, so an abbreviated
984+ # spelling such as `--upl` for `--upload-pack` must be rejected too. An
985+ # option is unsafe if its canonical name is a prefix of any blocked
986+ # option's canonical name. Only long options and multi-character kwargs
987+ # can be abbreviations; single-character short options remain exact-match
988+ # only.
970989 canonical_unsafe_options = {cls ._canonicalize_option_name (option ): option for option in unsafe_options }
990+ unsafe_short_options = {
991+ canonical : option
992+ for canonical , option in canonical_unsafe_options .items ()
993+ if option .startswith ("-" ) and not option .startswith ("--" ) and len (canonical ) == 1
994+ }
995+ # These value-less Git flags can be clustered before another short option
996+ # (for example, ``-fuVALUE``). Stop at any other character because it may
997+ # begin an attached value, as ``o`` does in the safe option ``-oupstream``.
998+ clusterable_short_options = frozenset ("46flnqsv" )
999+ options_are_kwargs = all (not option .startswith ("-" ) for option in options )
9711000 for option in options :
972- unsafe_option = canonical_unsafe_options .get (cls ._canonicalize_option_name (option ))
1001+ candidate = cls ._canonicalize_option_name (option )
1002+ if not candidate :
1003+ continue
1004+ unsafe_option = canonical_unsafe_options .get (candidate )
9731005 if unsafe_option is not None :
9741006 raise UnsafeOptionError (f"{ unsafe_option } is not allowed, use `allow_unsafe_options=True` to allow it." )
1007+ option_token = option .split ("=" , 1 )[0 ].split (None , 1 )[0 ]
1008+ if option_token .startswith ("-" ) and not option_token .startswith ("--" ):
1009+ for option_char in option_token [1 :]:
1010+ unsafe_option = unsafe_short_options .get (option_char )
1011+ if unsafe_option is not None :
1012+ raise UnsafeOptionError (
1013+ f"{ unsafe_option } is not allowed, use `allow_unsafe_options=True` to allow it."
1014+ )
1015+ if option_char not in clusterable_short_options :
1016+ break
1017+ if not (option .startswith ("--" ) or (options_are_kwargs and len (candidate ) > 1 )):
1018+ continue
1019+ for canonical , unsafe_option in canonical_unsafe_options .items ():
1020+ if canonical .startswith (candidate ):
1021+ raise UnsafeOptionError (
1022+ f"{ unsafe_option } is not allowed, use `allow_unsafe_options=True` to allow it."
1023+ )
9751024
9761025 AutoInterrupt : TypeAlias = _AutoInterrupt
9771026
0 commit comments