| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -961,17 +961,66 @@ def _canonicalize_option_name(cls, option: str) -> str: | |||
| 961 | 961 | ||
| 962 | 962 | @classmethod | |
| 963 | 963 | 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. | ||
| 968 | 981 | """ | |
| 969 | 982 | # 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. | ||
| 970 | 989 | 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) | ||
| 971 | 1000 | 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) | ||
| 973 | 1005 | if unsafe_option is not None: | |
| 974 | 1006 | 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 | + ) | ||
| 975 | 1024 | ||
| 976 | 1025 | AutoInterrupt: TypeAlias = _AutoInterrupt | |
| 977 | 1026 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -118,8 +118,13 @@ def test_clone_unsafe_options(self, rw_repo): | |||
| 118 | 118 | unsafe_options = [ | |
| 119 | 119 | f"--upload-pack='touch {tmp_file}'", | |
| 120 | 120 | f"-u 'touch {tmp_file}'", | |
| 121 | + f"-utouch {tmp_file}; false", | ||
| 122 | + f"-futouch${{IFS}}{tmp_file}; false", | ||
| 123 | + f"-qutouch${{IFS}}{tmp_file}; false", | ||
| 121 | 124 | "--config=protocol.ext.allow=always", | |
| 122 | 125 | "-c protocol.ext.allow=always", | |
| 126 | + "-cprotocol.ext.allow=always", | ||
| 127 | + "-vcprotocol.ext.allow=always", | ||
| 123 | 128 | ] | |
| 124 | 129 | for unsafe_option in unsafe_options: | |
| 125 | 130 | with self.assertRaises(UnsafeOptionError): | |
@@ -138,6 +143,31 @@ def test_clone_unsafe_options(self, rw_repo): | |||
| 138 | 143 | rw_repo.clone(tmp_dir, **unsafe_option) | |
| 139 | 144 | assert not tmp_file.exists() | |
| 140 | 145 | ||
| 146 | + @with_rw_repo("HEAD") | ||
| 147 | + def test_clone_unsafe_options_abbreviated(self, rw_repo): | ||
| 148 | + with tempfile.TemporaryDirectory() as tdir: | ||
| 149 | + tmp_dir = pathlib.Path(tdir) | ||
| 150 | + tmp_file = tmp_dir / "pwn" | ||
| 151 | + unsafe_options = [ | ||
| 152 | + f"--upl='touch {tmp_file}'", | ||
| 153 | + f"--upload-pac='touch {tmp_file}'", | ||
| 154 | + "--conf=protocol.ext.allow=always", | ||
| 155 | + ] | ||
| 156 | + for unsafe_option in unsafe_options: | ||
| 157 | + with self.assertRaises(UnsafeOptionError): | ||
| 158 | + rw_repo.clone(tmp_dir, multi_options=[unsafe_option]) | ||
| 159 | + assert not tmp_file.exists() | ||
| 160 | + | ||
| 161 | + unsafe_kwargs = [ | ||
| 162 | + {"upl": f"touch {tmp_file}"}, | ||
| 163 | + {"upload_pac": f"touch {tmp_file}"}, | ||
| 164 | + {"conf": "protocol.ext.allow=always"}, | ||
| 165 | + ] | ||
| 166 | + for unsafe_option in unsafe_kwargs: | ||
| 167 | + with self.assertRaises(UnsafeOptionError): | ||
| 168 | + rw_repo.clone(tmp_dir, **unsafe_option) | ||
| 169 | + assert not tmp_file.exists() | ||
| 170 | + | ||
| 141 | 171 | @with_rw_repo("HEAD") | |
| 142 | 172 | def test_clone_unsafe_options_are_checked_after_splitting_multi_options(self, rw_repo): | |
| 143 | 173 | with tempfile.TemporaryDirectory() as tdir: | |
@@ -191,7 +221,9 @@ def test_clone_safe_options(self, rw_repo): | |||
| 191 | 221 | options = [ | |
| 192 | 222 | "--depth=1", | |
| 193 | 223 | "--single-branch", | |
| 224 | + "--origin upload", | ||
| 194 | 225 | "-q", | |
| 226 | + "-oupstream", | ||
| 195 | 227 | ] | |
| 196 | 228 | for option in options: | |
| 197 | 229 | destination = tmp_dir / option | |
@@ -207,8 +239,13 @@ def test_clone_from_unsafe_options(self, rw_repo): | |||
| 207 | 239 | unsafe_options = [ | |
| 208 | 240 | f"--upload-pack='touch {tmp_file}'", | |
| 209 | 241 | f"-u 'touch {tmp_file}'", | |
| 242 | + f"-utouch {tmp_file}; false", | ||
| 243 | + f"-futouch${{IFS}}{tmp_file}; false", | ||
| 244 | + f"-qutouch${{IFS}}{tmp_file}; false", | ||
| 210 | 245 | "--config=protocol.ext.allow=always", | |
| 211 | 246 | "-c protocol.ext.allow=always", | |
| 247 | + "-cprotocol.ext.allow=always", | ||
| 248 | + "-vcprotocol.ext.allow=always", | ||
| 212 | 249 | ] | |
| 213 | 250 | for unsafe_option in unsafe_options: | |
| 214 | 251 | with self.assertRaises(UnsafeOptionError): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -170,6 +170,34 @@ def test_check_unsafe_options_normalizes_kwargs(self): | |||
| 170 | 170 | with self.assertRaises(UnsafeOptionError): | |
| 171 | 171 | Git.check_unsafe_options(options=options, unsafe_options=unsafe_options) | |
| 172 | 172 | ||
| 173 | + def test_check_unsafe_options_does_not_treat_short_options_as_abbreviations(self): | ||
| 174 | + unsafe_options = ["--upload-pack"] | ||
| 175 | + | ||
| 176 | + Git.check_unsafe_options(options=["-u", "u", "-upl"], unsafe_options=unsafe_options) | ||
| 177 | + with self.assertRaises(UnsafeOptionError): | ||
| 178 | + Git.check_unsafe_options(options=["--u"], unsafe_options=unsafe_options) | ||
| 179 | + | ||
| 180 | + def test_check_unsafe_options_does_not_treat_option_values_as_abbreviations(self): | ||
| 181 | + Git.check_unsafe_options(options=["--branch", "conf"], unsafe_options=["--config"]) | ||
| 182 | + | ||
| 183 | + def test_check_unsafe_options_rejects_joined_unsafe_short_options(self): | ||
| 184 | + cases = [ | ||
| 185 | + (["-utouch /tmp/pwn"], ["-u"]), | ||
| 186 | + (["-futouch /tmp/pwn"], ["-u"]), | ||
| 187 | + (["-qutouch${IFS}/tmp/pwn"], ["-u"]), | ||
| 188 | + (["-cprotocol.ext.allow=always"], ["-c"]), | ||
| 189 | + (["-vcprotocol.ext.allow=always"], ["-c"]), | ||
| 190 | + ] | ||
| 191 | + | ||
| 192 | + for options, unsafe_options in cases: | ||
| 193 | + with self.assertRaises(UnsafeOptionError): | ||
| 194 | + Git.check_unsafe_options(options=options, unsafe_options=unsafe_options) | ||
| 195 | + | ||
| 196 | + def test_check_unsafe_options_allows_attached_safe_short_option_values(self): | ||
| 197 | + unsafe_options = ["--upload-pack", "-u", "--config", "-c"] | ||
| 198 | + | ||
| 199 | + Git.check_unsafe_options(options=["-oupstream", "-bcurrent"], unsafe_options=unsafe_options) | ||
| 200 | + | ||
| 173 | 201 | _shell_cases = ( | |
| 174 | 202 | # value_in_call, value_from_class, expected_popen_arg | |
| 175 | 203 | (None, False, False), | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -832,7 +832,11 @@ def test_fetch_unsafe_options(self, rw_repo): | |||
| 832 | 832 | remote = rw_repo.remote("origin") | |
| 833 | 833 | tmp_dir = Path(tdir) | |
| 834 | 834 | tmp_file = tmp_dir / "pwn" | |
| 835 | - unsafe_options = [{"upload-pack": f"touch {tmp_file}"}, {"upload_pack": f"touch {tmp_file}"}] | ||
| 835 | + unsafe_options = [ | ||
| 836 | + {"upload-pack": f"touch {tmp_file}"}, | ||
| 837 | + {"upload_pack": f"touch {tmp_file}"}, | ||
| 838 | + {"upload_p": f"touch {tmp_file}"}, | ||
| 839 | + ] | ||
| 836 | 840 | for unsafe_option in unsafe_options: | |
| 837 | 841 | with self.assertRaises(UnsafeOptionError): | |
| 838 | 842 | remote.fetch(**unsafe_option) | |
@@ -900,7 +904,11 @@ def test_pull_unsafe_options(self, rw_repo): | |||
| 900 | 904 | remote = rw_repo.remote("origin") | |
| 901 | 905 | tmp_dir = Path(tdir) | |
| 902 | 906 | tmp_file = tmp_dir / "pwn" | |
| 903 | - unsafe_options = [{"upload-pack": f"touch {tmp_file}"}, {"upload_pack": f"touch {tmp_file}"}] | ||
| 907 | + unsafe_options = [ | ||
| 908 | + {"upload-pack": f"touch {tmp_file}"}, | ||
| 909 | + {"upload_pack": f"touch {tmp_file}"}, | ||
| 910 | + {"upload_p": f"touch {tmp_file}"}, | ||
| 911 | + ] | ||
| 904 | 912 | for unsafe_option in unsafe_options: | |
| 905 | 913 | with self.assertRaises(UnsafeOptionError): | |
| 906 | 914 | remote.pull(**unsafe_option) | |
@@ -971,7 +979,9 @@ def test_push_unsafe_options(self, rw_repo): | |||
| 971 | 979 | unsafe_options = [ | |
| 972 | 980 | {"receive-pack": f"touch {tmp_file}"}, | |
| 973 | 981 | {"receive_pack": f"touch {tmp_file}"}, | |
| 982 | + {"receive_p": f"touch {tmp_file}"}, | ||
| 974 | 983 | {"exec": f"touch {tmp_file}"}, | |
| 984 | + {"exe": f"touch {tmp_file}"}, | ||
| 975 | 985 | ] | |
| 976 | 986 | for unsafe_option in unsafe_options: | |
| 977 | 987 | assert not tmp_file.exists() | |
| Back | FazBrowse Home | New Git URL |
0 commit comments