| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,6 +4,7 @@ | |||
| 4 | 4 | # This module is part of GitPython and is released under | |
| 5 | 5 | # the BSD License: http://www.opensource.org/licenses/bsd-license.php | |
| 6 | 6 | from __future__ import annotations | |
| 7 | + import re | ||
| 7 | 8 | from contextlib import contextmanager | |
| 8 | 9 | import io | |
| 9 | 10 | import logging | |
@@ -24,7 +25,7 @@ | |||
| 24 | 25 | from git.exc import CommandError | |
| 25 | 26 | from git.util import is_cygwin_git, cygpath, expand_path, remove_password_if_present | |
| 26 | 27 | ||
| 27 | - from .exc import GitCommandError, GitCommandNotFound | ||
| 28 | + from .exc import GitCommandError, GitCommandNotFound, UnsafeOptionError, UnsafeProtocolError | ||
| 28 | 29 | from .util import ( | |
| 29 | 30 | LazyMixin, | |
| 30 | 31 | stream_copy, | |
@@ -262,6 +263,8 @@ class Git(LazyMixin): | |||
| 262 | 263 | ||
| 263 | 264 | _excluded_ = ("cat_file_all", "cat_file_header", "_version_info") | |
| 264 | 265 | ||
| 266 | + re_unsafe_protocol = re.compile("(.+)::.+") | ||
| 267 | + | ||
| 265 | 268 | def __getstate__(self) -> Dict[str, Any]: | |
| 266 | 269 | return slots_to_dict(self, exclude=self._excluded_) | |
| 267 | 270 | ||
@@ -454,6 +457,48 @@ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike: | |||
| 454 | 457 | url = url.replace("\\\\", "\\").replace("\\", "/") | |
| 455 | 458 | return url | |
| 456 | 459 | ||
| 460 | + @classmethod | ||
| 461 | + def check_unsafe_protocols(cls, url: str) -> None: | ||
| 462 | + """ | ||
| 463 | + Check for unsafe protocols. | ||
| 464 | + | ||
| 465 | + Apart from the usual protocols (http, git, ssh), | ||
| 466 | + Git allows "remote helpers" that have the form `<transport>::<address>`, | ||
| 467 | + one of these helpers (`ext::`) can be used to invoke any arbitrary command. | ||
| 468 | + | ||
| 469 | + See: | ||
| 470 | + | ||
| 471 | + - https://git-scm.com/docs/gitremote-helpers | ||
| 472 | + - https://git-scm.com/docs/git-remote-ext | ||
| 473 | + """ | ||
| 474 | + match = cls.re_unsafe_protocol.match(url) | ||
| 475 | + if match: | ||
| 476 | + protocol = match.group(1) | ||
| 477 | + raise UnsafeProtocolError( | ||
| 478 | + f"The `{protocol}::` protocol looks suspicious, use `allow_unsafe_protocols=True` to allow it." | ||
| 479 | + ) | ||
| 480 | + | ||
| 481 | + @classmethod | ||
| 482 | + def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None: | ||
| 483 | + """ | ||
| 484 | + Check for unsafe options. | ||
| 485 | + | ||
| 486 | + Some options that are passed to `git <command>` can be used to execute | ||
| 487 | + arbitrary commands, this are blocked by default. | ||
| 488 | + """ | ||
| 489 | + # Options can be of the form `foo` or `--foo bar` `--foo=bar`, | ||
| 490 | + # so we need to check if they start with "--foo" or if they are equal to "foo". | ||
| 491 | + bare_options = [ | ||
| 492 | + option.lstrip("-") | ||
| 493 | + for option in unsafe_options | ||
| 494 | + ] | ||
| 495 | + for option in options: | ||
| 496 | + for unsafe_option, bare_option in zip(unsafe_options, bare_options): | ||
| 497 | + if option.startswith(unsafe_option) or option == bare_option: | ||
| 498 | + raise UnsafeOptionError( | ||
| 499 | + f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it." | ||
| 500 | + ) | ||
| 501 | + | ||
| 457 | 502 | class AutoInterrupt(object): | |
| 458 | 503 | """Kill/Interrupt the stored process instance once this instance goes out of scope. It is | |
| 459 | 504 | used to prevent processes piling up in case iterators stop reading. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -37,8 +37,12 @@ class NoSuchPathError(GitError, OSError): | |||
| 37 | 37 | """Thrown if a path could not be access by the system.""" | |
| 38 | 38 | ||
| 39 | 39 | ||
| 40 | - class UnsafeOptionsUsedError(GitError): | ||
| 41 | - """Thrown if unsafe protocols or options are passed without overridding.""" | ||
| 40 | + class UnsafeProtocolError(GitError): | ||
| 41 | + """Thrown if unsafe protocols are passed without being explicitly allowed.""" | ||
| 42 | + | ||
| 43 | + | ||
| 44 | + class UnsafeOptionError(GitError): | ||
| 45 | + """Thrown if unsafe options are passed without being explicitly allowed.""" | ||
| 42 | 46 | ||
| 43 | 47 | ||
| 44 | 48 | class CommandError(GitError): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -535,6 +535,23 @@ class Remote(LazyMixin, IterableObj): | |||
| 535 | 535 | __slots__ = ("repo", "name", "_config_reader") | |
| 536 | 536 | _id_attribute_ = "name" | |
| 537 | 537 | ||
| 538 | + unsafe_git_fetch_options = [ | ||
| 539 | + # This option allows users to execute arbitrary commands. | ||
| 540 | + # https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt---upload-packltupload-packgt | ||
| 541 | + "--upload-pack", | ||
| 542 | + ] | ||
| 543 | + unsafe_git_pull_options = [ | ||
| 544 | + # This option allows users to execute arbitrary commands. | ||
| 545 | + # https://git-scm.com/docs/git-pull#Documentation/git-pull.txt---upload-packltupload-packgt | ||
| 546 | + "--upload-pack" | ||
| 547 | + ] | ||
| 548 | + unsafe_git_push_options = [ | ||
| 549 | + # This option allows users to execute arbitrary commands. | ||
| 550 | + # https://git-scm.com/docs/git-push#Documentation/git-push.txt---execltgit-receive-packgt | ||
| 551 | + "--receive-pack", | ||
| 552 | + "--exec", | ||
| 553 | + ] | ||
| 554 | + | ||
| 538 | 555 | def __init__(self, repo: "Repo", name: str) -> None: | |
| 539 | 556 | """Initialize a remote instance | |
| 540 | 557 | ||
@@ -611,7 +628,9 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator["Remote | |||
| 611 | 628 | yield Remote(repo, section[lbound + 1 : rbound]) | |
| 612 | 629 | # END for each configuration section | |
| 613 | 630 | ||
| 614 | - def set_url(self, new_url: str, old_url: Optional[str] = None, **kwargs: Any) -> "Remote": | ||
| 631 | + def set_url( | ||
| 632 | + self, new_url: str, old_url: Optional[str] = None, allow_unsafe_protocols: bool = False, **kwargs: Any | ||
| 633 | + ) -> "Remote": | ||
| 615 | 634 | """Configure URLs on current remote (cf command git remote set_url) | |
| 616 | 635 | ||
| 617 | 636 | This command manages URLs on the remote. | |
@@ -620,15 +639,17 @@ def set_url(self, new_url: str, old_url: Optional[str] = None, **kwargs: Any) -> | |||
| 620 | 639 | :param old_url: when set, replaces this URL with new_url for the remote | |
| 621 | 640 | :return: self | |
| 622 | 641 | """ | |
| 642 | + if not allow_unsafe_protocols: | ||
| 643 | + Git.check_unsafe_protocols(new_url) | ||
| 623 | 644 | scmd = "set-url" | |
| 624 | 645 | kwargs["insert_kwargs_after"] = scmd | |
| 625 | 646 | if old_url: | |
| 626 | - self.repo.git.remote(scmd, self.name, new_url, old_url, **kwargs) | ||
| 647 | + self.repo.git.remote(scmd, "--", self.name, new_url, old_url, **kwargs) | ||
| 627 | 648 | else: | |
| 628 | - self.repo.git.remote(scmd, self.name, new_url, **kwargs) | ||
| 649 | + self.repo.git.remote(scmd, "--", self.name, new_url, **kwargs) | ||
| 629 | 650 | return self | |
| 630 | 651 | ||
| 631 | - def add_url(self, url: str, **kwargs: Any) -> "Remote": | ||
| 652 | + def add_url(self, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote": | ||
| 632 | 653 | """Adds a new url on current remote (special case of git remote set_url) | |
| 633 | 654 | ||
| 634 | 655 | This command adds new URLs to a given remote, making it possible to have | |
@@ -637,6 +658,8 @@ def add_url(self, url: str, **kwargs: Any) -> "Remote": | |||
| 637 | 658 | :param url: string being the URL to add as an extra remote URL | |
| 638 | 659 | :return: self | |
| 639 | 660 | """ | |
| 661 | + if not allow_unsafe_protocols: | ||
| 662 | + Git.check_unsafe_protocols(url) | ||
| 640 | 663 | return self.set_url(url, add=True) | |
| 641 | 664 | ||
| 642 | 665 | def delete_url(self, url: str, **kwargs: Any) -> "Remote": | |
@@ -729,7 +752,7 @@ def stale_refs(self) -> IterableList[Reference]: | |||
| 729 | 752 | return out_refs | |
| 730 | 753 | ||
| 731 | 754 | @classmethod | |
| 732 | - def create(cls, repo: "Repo", name: str, url: str, **kwargs: Any) -> "Remote": | ||
| 755 | + def create(cls, repo: "Repo", name: str, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote": | ||
| 733 | 756 | """Create a new remote to the given repository | |
| 734 | 757 | :param repo: Repository instance that is to receive the new remote | |
| 735 | 758 | :param name: Desired name of the remote | |
@@ -739,7 +762,10 @@ def create(cls, repo: "Repo", name: str, url: str, **kwargs: Any) -> "Remote": | |||
| 739 | 762 | :raise GitCommandError: in case an origin with that name already exists""" | |
| 740 | 763 | scmd = "add" | |
| 741 | 764 | kwargs["insert_kwargs_after"] = scmd | |
| 742 | - repo.git.remote(scmd, name, Git.polish_url(url), **kwargs) | ||
| 765 | + url = Git.polish_url(url) | ||
| 766 | + if not allow_unsafe_protocols: | ||
| 767 | + Git.check_unsafe_protocols(url) | ||
| 768 | + repo.git.remote(scmd, "--", name, url, **kwargs) | ||
| 743 | 769 | return cls(repo, name) | |
| 744 | 770 | ||
| 745 | 771 | # add is an alias | |
@@ -921,6 +947,8 @@ def fetch( | |||
| 921 | 947 | progress: Union[RemoteProgress, None, "UpdateProgress"] = None, | |
| 922 | 948 | verbose: bool = True, | |
| 923 | 949 | kill_after_timeout: Union[None, float] = None, | |
| 950 | + allow_unsafe_protocols: bool = False, | ||
| 951 | + allow_unsafe_options: bool = False, | ||
| 924 | 952 | **kwargs: Any, | |
| 925 | 953 | ) -> IterableList[FetchInfo]: | |
| 926 | 954 | """Fetch the latest changes for this remote | |
@@ -963,6 +991,14 @@ def fetch( | |||
| 963 | 991 | else: | |
| 964 | 992 | args = [refspec] | |
| 965 | 993 | ||
| 994 | + if not allow_unsafe_protocols: | ||
| 995 | + for ref in args: | ||
| 996 | + if ref: | ||
| 997 | + Git.check_unsafe_protocols(ref) | ||
| 998 | + | ||
| 999 | + if not allow_unsafe_options: | ||
| 1000 | + Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_fetch_options) | ||
| 1001 | + | ||
| 966 | 1002 | proc = self.repo.git.fetch( | |
| 967 | 1003 | "--", self, *args, as_process=True, with_stdout=False, universal_newlines=True, v=verbose, **kwargs | |
| 968 | 1004 | ) | |
@@ -976,6 +1012,8 @@ def pull( | |||
| 976 | 1012 | refspec: Union[str, List[str], None] = None, | |
| 977 | 1013 | progress: Union[RemoteProgress, "UpdateProgress", None] = None, | |
| 978 | 1014 | kill_after_timeout: Union[None, float] = None, | |
| 1015 | + allow_unsafe_protocols: bool = False, | ||
| 1016 | + allow_unsafe_options: bool = False, | ||
| 979 | 1017 | **kwargs: Any, | |
| 980 | 1018 | ) -> IterableList[FetchInfo]: | |
| 981 | 1019 | """Pull changes from the given branch, being the same as a fetch followed | |
@@ -990,6 +1028,16 @@ def pull( | |||
| 990 | 1028 | # No argument refspec, then ensure the repo's config has a fetch refspec. | |
| 991 | 1029 | self._assert_refspec() | |
| 992 | 1030 | kwargs = add_progress(kwargs, self.repo.git, progress) | |
| 1031 | + | ||
| 1032 | + if not allow_unsafe_protocols and refspec: | ||
| 1033 | + if isinstance(refspec, str): | ||
| 1034 | + Git.check_unsafe_protocols(refspec) | ||
| 1035 | + else: | ||
| 1036 | + for ref in refspec: | ||
| 1037 | + Git.check_unsafe_protocols(ref) | ||
| 1038 | + if not allow_unsafe_options: | ||
| 1039 | + Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_pull_options) | ||
| 1040 | + | ||
| 993 | 1041 | proc = self.repo.git.pull( | |
| 994 | 1042 | "--", self, refspec, with_stdout=False, as_process=True, universal_newlines=True, v=True, **kwargs | |
| 995 | 1043 | ) | |
@@ -1003,6 +1051,8 @@ def push( | |||
| 1003 | 1051 | refspec: Union[str, List[str], None] = None, | |
| 1004 | 1052 | progress: Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None] = None, | |
| 1005 | 1053 | kill_after_timeout: Union[None, float] = None, | |
| 1054 | + allow_unsafe_protocols: bool = False, | ||
| 1055 | + allow_unsafe_options: bool = False, | ||
| 1006 | 1056 | **kwargs: Any, | |
| 1007 | 1057 | ) -> IterableList[PushInfo]: | |
| 1008 | 1058 | """Push changes from source branch in refspec to target branch in refspec. | |
@@ -1033,6 +1083,17 @@ def push( | |||
| 1033 | 1083 | If the operation fails completely, the length of the returned IterableList will | |
| 1034 | 1084 | be 0.""" | |
| 1035 | 1085 | kwargs = add_progress(kwargs, self.repo.git, progress) | |
| 1086 | + | ||
| 1087 | + if not allow_unsafe_protocols and refspec: | ||
| 1088 | + if isinstance(refspec, str): | ||
| 1089 | + Git.check_unsafe_protocols(refspec) | ||
| 1090 | + else: | ||
| 1091 | + for ref in refspec: | ||
| 1092 | + Git.check_unsafe_protocols(ref) | ||
| 1093 | + | ||
| 1094 | + if not allow_unsafe_options: | ||
| 1095 | + Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_push_options) | ||
| 1096 | + | ||
| 1036 | 1097 | proc = self.repo.git.push( | |
| 1037 | 1098 | "--", | |
| 1038 | 1099 | self, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -25,7 +25,6 @@ | |||
| 25 | 25 | GitCommandError, | |
| 26 | 26 | InvalidGitRepositoryError, | |
| 27 | 27 | NoSuchPathError, | |
| 28 | - UnsafeOptionsUsedError, | ||
| 29 | 28 | ) | |
| 30 | 29 | from git.index import IndexFile | |
| 31 | 30 | from git.objects import Submodule, RootModule, Commit | |
@@ -133,7 +132,18 @@ class Repo(object): | |||
| 133 | 132 | re_envvars = re.compile(r"(\$(\{\s?)?[a-zA-Z_]\w*(\}\s?)?|%\s?[a-zA-Z_]\w*\s?%)") | |
| 134 | 133 | re_author_committer_start = re.compile(r"^(author|committer)") | |
| 135 | 134 | re_tab_full_line = re.compile(r"^\t(.*)$") | |
| 136 | - re_config_protocol_option = re.compile(r"-[-]?c(|onfig)\s+protocol\.", re.I) | ||
| 135 | + | ||
| 136 | + unsafe_git_clone_options = [ | ||
| 137 | + # This option allows users to execute arbitrary commands. | ||
| 138 | + # https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---upload-packltupload-packgt | ||
| 139 | + "--upload-pack", | ||
| 140 | + "-u", | ||
| 141 | + # Users can override configuration variables | ||
| 142 | + # like `protocol.allow` or `core.gitProxy` to execute arbitrary commands. | ||
| 143 | + # https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---configltkeygtltvaluegt | ||
| 144 | + "--config", | ||
| 145 | + "-c", | ||
| 146 | + ] | ||
| 137 | 147 | ||
| 138 | 148 | # invariants | |
| 139 | 149 | # represents the configuration level of a configuration file | |
@@ -961,7 +971,7 @@ def blame( | |||
| 961 | 971 | file: str, | |
| 962 | 972 | incremental: bool = False, | |
| 963 | 973 | rev_opts: Optional[List[str]] = None, | |
| 964 | - **kwargs: Any | ||
| 974 | + **kwargs: Any, | ||
| 965 | 975 | ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None: | |
| 966 | 976 | """The blame information for the given file at the given revision. | |
| 967 | 977 | ||
@@ -1152,6 +1162,8 @@ def _clone( | |||
| 1152 | 1162 | odb_default_type: Type[GitCmdObjectDB], | |
| 1153 | 1163 | progress: Union["RemoteProgress", "UpdateProgress", Callable[..., "RemoteProgress"], None] = None, | |
| 1154 | 1164 | multi_options: Optional[List[str]] = None, | |
| 1165 | + allow_unsafe_protocols: bool = False, | ||
| 1166 | + allow_unsafe_options: bool = False, | ||
| 1155 | 1167 | **kwargs: Any, | |
| 1156 | 1168 | ) -> "Repo": | |
| 1157 | 1169 | odbt = kwargs.pop("odbt", odb_default_type) | |
@@ -1173,6 +1185,12 @@ def _clone( | |||
| 1173 | 1185 | multi = None | |
| 1174 | 1186 | if multi_options: | |
| 1175 | 1187 | multi = shlex.split(" ".join(multi_options)) | |
| 1188 | + | ||
| 1189 | + if not allow_unsafe_protocols: | ||
| 1190 | + Git.check_unsafe_protocols(str(url)) | ||
| 1191 | + if not allow_unsafe_options and multi_options: | ||
| 1192 | + Git.check_unsafe_options(options=multi_options, unsafe_options=cls.unsafe_git_clone_options) | ||
| 1193 | + | ||
| 1176 | 1194 | proc = git.clone( | |
| 1177 | 1195 | multi, | |
| 1178 | 1196 | "--", | |
@@ -1221,27 +1239,13 @@ def _clone( | |||
| 1221 | 1239 | # END handle remote repo | |
| 1222 | 1240 | return repo | |
| 1223 | 1241 | ||
| 1224 | - @classmethod | ||
| 1225 | - def unsafe_options( | ||
| 1226 | - cls, | ||
| 1227 | - url: str, | ||
| 1228 | - multi_options: Optional[List[str]] = None, | ||
| 1229 | - ) -> bool: | ||
| 1230 | - if "ext::" in url: | ||
| 1231 | - return True | ||
| 1232 | - if multi_options is not None: | ||
| 1233 | - if any(["--upload-pack" in m for m in multi_options]): | ||
| 1234 | - return True | ||
| 1235 | - if any([re.match(cls.re_config_protocol_option, m) for m in multi_options]): | ||
| 1236 | - return True | ||
| 1237 | - return False | ||
| 1238 | - | ||
| 1239 | 1242 | def clone( | |
| 1240 | 1243 | self, | |
| 1241 | 1244 | path: PathLike, | |
| 1242 | 1245 | progress: Optional[Callable] = None, | |
| 1243 | 1246 | multi_options: Optional[List[str]] = None, | |
| 1244 | - unsafe_protocols: bool = False, | ||
| 1247 | + allow_unsafe_protocols: bool = False, | ||
| 1248 | + allow_unsafe_options: bool = False, | ||
| 1245 | 1249 | **kwargs: Any, | |
| 1246 | 1250 | ) -> "Repo": | |
| 1247 | 1251 | """Create a clone from this repository. | |
@@ -1259,15 +1263,15 @@ def clone( | |||
| 1259 | 1263 | * All remaining keyword arguments are given to the git-clone command | |
| 1260 | 1264 | ||
| 1261 | 1265 | :return: ``git.Repo`` (the newly cloned repo)""" | |
| 1262 | - if not unsafe_protocols and self.unsafe_options(path, multi_options): | ||
| 1263 | - raise UnsafeOptionsUsedError(f"{path} requires unsafe_protocols flag") | ||
| 1264 | 1266 | return self._clone( | |
| 1265 | 1267 | self.git, | |
| 1266 | 1268 | self.common_dir, | |
| 1267 | 1269 | path, | |
| 1268 | 1270 | type(self.odb), | |
| 1269 | 1271 | progress, | |
| 1270 | 1272 | multi_options, | |
| 1273 | + allow_unsafe_protocols=allow_unsafe_protocols, | ||
| 1274 | + allow_unsafe_options=allow_unsafe_options, | ||
| 1271 | 1275 | **kwargs, | |
| 1272 | 1276 | ) | |
| 1273 | 1277 | ||
@@ -1279,7 +1283,8 @@ def clone_from( | |||
| 1279 | 1283 | progress: Optional[Callable] = None, | |
| 1280 | 1284 | env: Optional[Mapping[str, str]] = None, | |
| 1281 | 1285 | multi_options: Optional[List[str]] = None, | |
| 1282 | - unsafe_protocols: bool = False, | ||
| 1286 | + allow_unsafe_protocols: bool = False, | ||
| 1287 | + allow_unsafe_options: bool = False, | ||
| 1283 | 1288 | **kwargs: Any, | |
| 1284 | 1289 | ) -> "Repo": | |
| 1285 | 1290 | """Create a clone from the given URL | |
@@ -1300,9 +1305,17 @@ def clone_from( | |||
| 1300 | 1305 | git = cls.GitCommandWrapperType(os.getcwd()) | |
| 1301 | 1306 | if env is not None: | |
| 1302 | 1307 | git.update_environment(**env) | |
| 1303 | - if not unsafe_protocols and cls.unsafe_options(url, multi_options): | ||
| 1304 | - raise UnsafeOptionsUsedError(f"{url} requires unsafe_protocols flag") | ||
| 1305 | - return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs) | ||
| 1308 | + return cls._clone( | ||
| 1309 | + git, | ||
| 1310 | + url, | ||
| 1311 | + to_path, | ||
| 1312 | + GitCmdObjectDB, | ||
| 1313 | + progress, | ||
| 1314 | + multi_options, | ||
| 1315 | + allow_unsafe_protocols=allow_unsafe_protocols, | ||
| 1316 | + allow_unsafe_options=allow_unsafe_options, | ||
| 1317 | + **kwargs, | ||
| 1318 | + ) | ||
| 1306 | 1319 | ||
| 1307 | 1320 | def archive( | |
| 1308 | 1321 | self, | |
| Back | FazBrowse Home | New Git URL |
0 commit comments