| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 65a7283 commit 701ce32
9 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -649,6 +649,11 @@ class Git(metaclass=_GitMeta): | |||
| 649 | 649 | ||
| 650 | 650 | re_unsafe_protocol = re.compile(r"(.+)::.+") | |
| 651 | 651 | ||
| 652 | + unsafe_git_ls_remote_options = [ | ||
| 653 | + # This option allows arbitrary command execution in git-ls-remote. | ||
| 654 | + "--upload-pack", | ||
| 655 | + ] | ||
| 656 | + | ||
| 652 | 657 | def __getstate__(self) -> Dict[str, Any]: | |
| 653 | 658 | return slots_to_dict(self, exclude=self._excluded_) | |
| 654 | 659 | ||
@@ -1022,6 +1027,20 @@ def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> | |||
| 1022 | 1027 | f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it." | |
| 1023 | 1028 | ) | |
| 1024 | 1029 | ||
| 1030 | + @classmethod | ||
| 1031 | + def _option_candidates(cls, args: Sequence[Any] = (), kwargs: Optional[Mapping[str, Any]] = None) -> List[str]: | ||
| 1032 | + """Collect possible option spellings before command-line transformation.""" | ||
| 1033 | + options = [ | ||
| 1034 | + option for option in cls._unpack_args([arg for arg in args if arg is not None]) if option.startswith("-") | ||
| 1035 | + ] | ||
| 1036 | + if kwargs: | ||
| 1037 | + for key, value in kwargs.items(): | ||
| 1038 | + values = value if isinstance(value, (list, tuple)) else (value,) | ||
| 1039 | + if any(value is True or (value is not False and value is not None) for value in values): | ||
| 1040 | + key = str(key) | ||
| 1041 | + options.append(f"-{key}" if len(key) == 1 else f"--{dashify(key)}") | ||
| 1042 | + return options | ||
| 1043 | + | ||
| 1025 | 1044 | AutoInterrupt: TypeAlias = _AutoInterrupt | |
| 1026 | 1045 | ||
| 1027 | 1046 | CatFileContentStream: TypeAlias = _CatFileContentStream | |
@@ -1079,6 +1098,22 @@ def set_persistent_git_options(self, **kwargs: Any) -> None: | |||
| 1079 | 1098 | ||
| 1080 | 1099 | self._persistent_git_options = self.transform_kwargs(split_single_char_options=True, **kwargs) | |
| 1081 | 1100 | ||
| 1101 | + def ls_remote( | ||
| 1102 | + self, | ||
| 1103 | + *args: Any, | ||
| 1104 | + allow_unsafe_options: bool = False, | ||
| 1105 | + **kwargs: Any, | ||
| 1106 | + ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: | ||
| 1107 | + """List references in a remote repository. | ||
| 1108 | + | ||
| 1109 | + :param allow_unsafe_options: | ||
| 1110 | + Allow unsafe options, like ``--upload-pack``. | ||
| 1111 | + """ | ||
| 1112 | + if not allow_unsafe_options: | ||
| 1113 | + candidate_options = self._option_candidates(args, kwargs) | ||
| 1114 | + Git.check_unsafe_options(options=candidate_options, unsafe_options=self.unsafe_git_ls_remote_options) | ||
| 1115 | + return self._call_process("ls_remote", *args, **kwargs) | ||
| 1116 | + | ||
| 1082 | 1117 | @property | |
| 1083 | 1118 | def working_dir(self) -> Union[None, PathLike]: | |
| 1084 | 1119 | """:return: Git directory we are working on""" | |
@@ -1585,7 +1620,7 @@ def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any | |||
| 1585 | 1620 | return args | |
| 1586 | 1621 | ||
| 1587 | 1622 | @classmethod | |
| 1588 | - def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]: | ||
| 1623 | + def _unpack_args(cls, arg_list: Sequence[Any]) -> List[str]: | ||
| 1589 | 1624 | outlist = [] | |
| 1590 | 1625 | if isinstance(arg_list, (list, tuple)): | |
| 1591 | 1626 | for arg in arg_list: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -86,6 +86,12 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable): | |||
| 86 | 86 | # INVARIANTS | |
| 87 | 87 | default_encoding = "UTF-8" | |
| 88 | 88 | ||
| 89 | + # Options to :manpage:`git-rev-list(1)` that can overwrite files. | ||
| 90 | + unsafe_git_rev_options = [ | ||
| 91 | + "--output", | ||
| 92 | + "-o", | ||
| 93 | + ] | ||
| 94 | + | ||
| 89 | 95 | type: Literal["commit"] = "commit" | |
| 90 | 96 | ||
| 91 | 97 | __slots__ = ( | |
@@ -302,6 +308,7 @@ def iter_items( | |||
| 302 | 308 | repo: "Repo", | |
| 303 | 309 | rev: Union[str, "Commit", "SymbolicReference"], | |
| 304 | 310 | paths: Union[PathLike, Sequence[PathLike]] = "", | |
| 311 | + allow_unsafe_options: bool = False, | ||
| 305 | 312 | **kwargs: Any, | |
| 306 | 313 | ) -> Iterator["Commit"]: | |
| 307 | 314 | R"""Find all commits matching the given criteria. | |
@@ -330,6 +337,11 @@ def iter_items( | |||
| 330 | 337 | raise ValueError("--pretty cannot be used as parsing expects single sha's only") | |
| 331 | 338 | # END handle pretty | |
| 332 | 339 | ||
| 340 | + if not allow_unsafe_options: | ||
| 341 | + Git.check_unsafe_options( | ||
| 342 | + options=Git._option_candidates([rev], kwargs), unsafe_options=cls.unsafe_git_rev_options | ||
| 343 | + ) | ||
| 344 | + | ||
| 333 | 345 | # Use -- in all cases, to prevent possibility of ambiguous arguments. | |
| 334 | 346 | # See https://github.com/gitpython-developers/GitPython/issues/264. | |
| 335 | 347 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1071,7 +1071,10 @@ def fetch( | |||
| 1071 | 1071 | Git.check_unsafe_protocols(ref) | |
| 1072 | 1072 | ||
| 1073 | 1073 | if not allow_unsafe_options: | |
| 1074 | - Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_fetch_options) | ||
| 1074 | + Git.check_unsafe_options( | ||
| 1075 | + options=Git._option_candidates([], kwargs), | ||
| 1076 | + unsafe_options=self.unsafe_git_fetch_options, | ||
| 1077 | + ) | ||
| 1075 | 1078 | ||
| 1076 | 1079 | proc = self.repo.git.fetch( | |
| 1077 | 1080 | "--", self, *args, as_process=True, with_stdout=False, universal_newlines=True, v=verbose, **kwargs | |
@@ -1125,7 +1128,10 @@ def pull( | |||
| 1125 | 1128 | Git.check_unsafe_protocols(ref) | |
| 1126 | 1129 | ||
| 1127 | 1130 | if not allow_unsafe_options: | |
| 1128 | - Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_pull_options) | ||
| 1131 | + Git.check_unsafe_options( | ||
| 1132 | + options=Git._option_candidates([], kwargs), | ||
| 1133 | + unsafe_options=self.unsafe_git_pull_options, | ||
| 1134 | + ) | ||
| 1129 | 1135 | ||
| 1130 | 1136 | proc = self.repo.git.pull( | |
| 1131 | 1137 | "--", self, refspec, with_stdout=False, as_process=True, universal_newlines=True, v=True, **kwargs | |
@@ -1198,7 +1204,10 @@ def push( | |||
| 1198 | 1204 | Git.check_unsafe_protocols(ref) | |
| 1199 | 1205 | ||
| 1200 | 1206 | if not allow_unsafe_options: | |
| 1201 | - Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_push_options) | ||
| 1207 | + Git.check_unsafe_options( | ||
| 1208 | + options=Git._option_candidates([], kwargs), | ||
| 1209 | + unsafe_options=self.unsafe_git_push_options, | ||
| 1210 | + ) | ||
| 1202 | 1211 | ||
| 1203 | 1212 | proc = self.repo.git.push( | |
| 1204 | 1213 | "--", | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -161,6 +161,20 @@ class Repo: | |||
| 161 | 161 | https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---configltkeygtltvaluegt | |
| 162 | 162 | """ | |
| 163 | 163 | ||
| 164 | + unsafe_git_archive_options = [ | ||
| 165 | + # Allows arbitrary command execution through the remote git-upload-archive command. | ||
| 166 | + "--exec", | ||
| 167 | + # Writes output to a caller-controlled filesystem path. | ||
| 168 | + "--output", | ||
| 169 | + "-o", | ||
| 170 | + ] | ||
| 171 | + | ||
| 172 | + unsafe_git_revision_options = [ | ||
| 173 | + # This option allows output to be written to arbitrary files before revision parsing. | ||
| 174 | + "--output", | ||
| 175 | + "-o", | ||
| 176 | + ] | ||
| 177 | + | ||
| 164 | 178 | # Invariants | |
| 165 | 179 | config_level: ConfigLevels_Tup = ("system", "user", "global", "repository") | |
| 166 | 180 | """Represents the configuration level of a configuration file.""" | |
@@ -775,6 +789,7 @@ def iter_commits( | |||
| 775 | 789 | self, | |
| 776 | 790 | rev: Union[str, Commit, "SymbolicReference", None] = None, | |
| 777 | 791 | paths: Union[PathLike, Sequence[PathLike]] = "", | |
| 792 | + allow_unsafe_options: bool = False, | ||
| 778 | 793 | **kwargs: Any, | |
| 779 | 794 | ) -> Iterator[Commit]: | |
| 780 | 795 | """An iterator of :class:`~git.objects.commit.Commit` objects representing the | |
@@ -792,6 +807,9 @@ def iter_commits( | |||
| 792 | 807 | Arguments to be passed to :manpage:`git-rev-list(1)`. | |
| 793 | 808 | Common ones are ``max_count`` and ``skip``. | |
| 794 | 809 | ||
| 810 | + :param allow_unsafe_options: | ||
| 811 | + Allow unsafe options in the revision argument, like ``--output``. | ||
| 812 | + | ||
| 795 | 813 | :note: | |
| 796 | 814 | To receive only commits between two named revisions, use the | |
| 797 | 815 | ``"revA...revB"`` revision specifier. | |
@@ -802,7 +820,18 @@ def iter_commits( | |||
| 802 | 820 | if rev is None: | |
| 803 | 821 | rev = self.head.commit | |
| 804 | 822 | ||
| 805 | - return Commit.iter_items(self, rev, paths, **kwargs) | ||
| 823 | + if not allow_unsafe_options: | ||
| 824 | + Git.check_unsafe_options( | ||
| 825 | + options=Git._option_candidates([rev], kwargs), unsafe_options=self.unsafe_git_revision_options | ||
| 826 | + ) | ||
| 827 | + | ||
| 828 | + return Commit.iter_items( | ||
| 829 | + self, | ||
| 830 | + rev, | ||
| 831 | + paths, | ||
| 832 | + allow_unsafe_options=allow_unsafe_options, | ||
| 833 | + **kwargs, | ||
| 834 | + ) | ||
| 806 | 835 | ||
| 807 | 836 | def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Commit]: | |
| 808 | 837 | R"""Find the closest common ancestor for the given revision | |
@@ -1079,7 +1108,9 @@ def active_branch(self) -> Head: | |||
| 1079 | 1108 | ) | |
| 1080 | 1109 | return active_branch | |
| 1081 | 1110 | ||
| 1082 | - def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) -> Iterator["BlameEntry"]: | ||
| 1111 | + def blame_incremental( | ||
| 1112 | + self, rev: str | HEAD | None, file: str, allow_unsafe_options: bool = False, **kwargs: Any | ||
| 1113 | + ) -> Iterator["BlameEntry"]: | ||
| 1083 | 1114 | """Iterator for blame information for the given file at the given revision. | |
| 1084 | 1115 | ||
| 1085 | 1116 | Unlike :meth:`blame`, this does not return the actual file's contents, only a | |
@@ -1090,6 +1121,9 @@ def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) -> | |||
| 1090 | 1121 | uncommitted changes. Otherwise, anything successfully parsed by | |
| 1091 | 1122 | :manpage:`git-rev-parse(1)` is a valid option. | |
| 1092 | 1123 | ||
| 1124 | + :param allow_unsafe_options: | ||
| 1125 | + Allow unsafe options in revision argument, like ``--output``. | ||
| 1126 | + | ||
| 1093 | 1127 | :return: | |
| 1094 | 1128 | Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates the | |
| 1095 | 1129 | commit to blame for the line, and range indicates a span of line numbers in | |
@@ -1098,6 +1132,10 @@ def blame_incremental(self, rev: str | HEAD | None, file: str, **kwargs: Any) -> | |||
| 1098 | 1132 | If you combine all line number ranges outputted by this command, you should get | |
| 1099 | 1133 | a continuous range spanning all line numbers in the file. | |
| 1100 | 1134 | """ | |
| 1135 | + if not allow_unsafe_options: | ||
| 1136 | + Git.check_unsafe_options( | ||
| 1137 | + options=Git._option_candidates([rev], kwargs), unsafe_options=self.unsafe_git_revision_options | ||
| 1138 | + ) | ||
| 1101 | 1139 | ||
| 1102 | 1140 | data: bytes = self.git.blame(rev, "--", file, p=True, incremental=True, stdout_as_string=False, **kwargs) | |
| 1103 | 1141 | commits: Dict[bytes, Commit] = {} | |
@@ -1176,7 +1214,8 @@ def blame( | |||
| 1176 | 1214 | rev: Union[str, HEAD, None], | |
| 1177 | 1215 | file: str, | |
| 1178 | 1216 | incremental: bool = False, | |
| 1179 | - rev_opts: Optional[List[str]] = None, | ||
| 1217 | + rev_opts: Optional[Sequence[str]] = None, | ||
| 1218 | + allow_unsafe_options: bool = False, | ||
| 1180 | 1219 | **kwargs: Any, | |
| 1181 | 1220 | ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None: | |
| 1182 | 1221 | """The blame information for the given file at the given revision. | |
@@ -1186,6 +1225,9 @@ def blame( | |||
| 1186 | 1225 | uncommitted changes. Otherwise, anything successfully parsed by | |
| 1187 | 1226 | :manpage:`git-rev-parse(1)` is a valid option. | |
| 1188 | 1227 | ||
| 1228 | + :param allow_unsafe_options: | ||
| 1229 | + Allow unsafe options in revision argument, like ``--output``. | ||
| 1230 | + | ||
| 1189 | 1231 | :return: | |
| 1190 | 1232 | list: [git.Commit, list: [<line>]] | |
| 1191 | 1233 | ||
@@ -1195,9 +1237,14 @@ def blame( | |||
| 1195 | 1237 | appearance. | |
| 1196 | 1238 | """ | |
| 1197 | 1239 | if incremental: | |
| 1198 | - return self.blame_incremental(rev, file, **kwargs) | ||
| 1199 | - rev_opts = rev_opts or [] | ||
| 1200 | - data: bytes = self.git.blame(rev, *rev_opts, "--", file, p=True, stdout_as_string=False, **kwargs) | ||
| 1240 | + return self.blame_incremental(rev, file, allow_unsafe_options=allow_unsafe_options, **kwargs) | ||
| 1241 | + rev_opts_list = list(rev_opts or []) | ||
| 1242 | + if not allow_unsafe_options: | ||
| 1243 | + Git.check_unsafe_options( | ||
| 1244 | + options=Git._option_candidates([rev, rev_opts_list], kwargs), | ||
| 1245 | + unsafe_options=self.unsafe_git_revision_options, | ||
| 1246 | + ) | ||
| 1247 | + data: bytes = self.git.blame(rev, *rev_opts_list, "--", file, p=True, stdout_as_string=False, **kwargs) | ||
| 1201 | 1248 | commits: Dict[str, Commit] = {} | |
| 1202 | 1249 | blames: List[List[Commit | List[str | bytes] | None]] = [] | |
| 1203 | 1250 | ||
@@ -1408,7 +1455,10 @@ def _clone( | |||
| 1408 | 1455 | if not allow_unsafe_protocols: | |
| 1409 | 1456 | Git.check_unsafe_protocols(url) | |
| 1410 | 1457 | if not allow_unsafe_options: | |
| 1411 | - Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=cls.unsafe_git_clone_options) | ||
| 1458 | + Git.check_unsafe_options( | ||
| 1459 | + options=Git._option_candidates([], kwargs), | ||
| 1460 | + unsafe_options=cls.unsafe_git_clone_options, | ||
| 1461 | + ) | ||
| 1412 | 1462 | if not allow_unsafe_options and multi: | |
| 1413 | 1463 | Git.check_unsafe_options(options=multi, unsafe_options=cls.unsafe_git_clone_options) | |
| 1414 | 1464 | ||
@@ -1583,6 +1633,8 @@ def archive( | |||
| 1583 | 1633 | ostream: Union[TextIO, BinaryIO], | |
| 1584 | 1634 | treeish: Optional[str] = None, | |
| 1585 | 1635 | prefix: Optional[str] = None, | |
| 1636 | + allow_unsafe_options: bool = False, | ||
| 1637 | + allow_unsafe_protocols: bool = False, | ||
| 1586 | 1638 | **kwargs: Any, | |
| 1587 | 1639 | ) -> Repo: | |
| 1588 | 1640 | """Archive the tree at the given revision. | |
@@ -1605,6 +1657,12 @@ def archive( | |||
| 1605 | 1657 | repository-relative path to a directory or file to place into the archive, | |
| 1606 | 1658 | or a list or tuple of multiple paths. | |
| 1607 | 1659 | ||
| 1660 | + :param allow_unsafe_options: | ||
| 1661 | + Allow unsafe options, like ``--exec`` or ``--output``. | ||
| 1662 | + | ||
| 1663 | + :param allow_unsafe_protocols: | ||
| 1664 | + Allow unsafe protocols to be used in ``remote``, like ``ext``. | ||
| 1665 | + | ||
| 1608 | 1666 | :raise git.exc.GitCommandError: | |
| 1609 | 1667 | If something went wrong. | |
| 1610 | 1668 | ||
@@ -1615,6 +1673,14 @@ def archive( | |||
| 1615 | 1673 | treeish = self.head.commit | |
| 1616 | 1674 | if prefix and "prefix" not in kwargs: | |
| 1617 | 1675 | kwargs["prefix"] = prefix | |
| 1676 | + remote = kwargs.get("remote") | ||
| 1677 | + if not allow_unsafe_protocols and remote is not None: | ||
| 1678 | + Git.check_unsafe_protocols(str(remote)) | ||
| 1679 | + if not allow_unsafe_options: | ||
| 1680 | + Git.check_unsafe_options( | ||
| 1681 | + options=Git._option_candidates([], kwargs), | ||
| 1682 | + unsafe_options=self.unsafe_git_archive_options, | ||
| 1683 | + ) | ||
| 1618 | 1684 | kwargs["output_stream"] = ostream | |
| 1619 | 1685 | path = kwargs.pop("path", []) | |
| 1620 | 1686 | path = cast(Union[PathLike, List[PathLike], Tuple[PathLike, ...]], path) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -117,11 +117,13 @@ def test_clone_unsafe_options(self, rw_repo): | |||
| 117 | 117 | tmp_file = tmp_dir / "pwn" | |
| 118 | 118 | unsafe_options = [ | |
| 119 | 119 | f"--upload-pack='touch {tmp_file}'", | |
| 120 | + f"--upl='touch {tmp_file}'", | ||
| 120 | 121 | f"-u 'touch {tmp_file}'", | |
| 121 | 122 | f"-utouch {tmp_file}; false", | |
| 122 | 123 | f"-futouch${{IFS}}{tmp_file}; false", | |
| 123 | 124 | f"-qutouch${{IFS}}{tmp_file}; false", | |
| 124 | 125 | "--config=protocol.ext.allow=always", | |
| 126 | + "--conf=protocol.ext.allow=always", | ||
| 125 | 127 | "-c protocol.ext.allow=always", | |
| 126 | 128 | "-cprotocol.ext.allow=always", | |
| 127 | 129 | "-vcprotocol.ext.allow=always", | |
@@ -134,8 +136,10 @@ def test_clone_unsafe_options(self, rw_repo): | |||
| 134 | 136 | unsafe_options = [ | |
| 135 | 137 | {"upload-pack": f"touch {tmp_file}"}, | |
| 136 | 138 | {"upload_pack": f"touch {tmp_file}"}, | |
| 139 | + {"upl": f"touch {tmp_file}"}, | ||
| 137 | 140 | {"u": f"touch {tmp_file}"}, | |
| 138 | 141 | {"config": "protocol.ext.allow=always"}, | |
| 142 | + {"conf": "protocol.ext.allow=always"}, | ||
| 139 | 143 | {"c": "protocol.ext.allow=always"}, | |
| 140 | 144 | ] | |
| 141 | 145 | for unsafe_option in unsafe_options: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6,6 +6,7 @@ | |||
| 6 | 6 | import copy | |
| 7 | 7 | from datetime import datetime | |
| 8 | 8 | from io import BytesIO | |
| 9 | + import tempfile | ||
| 9 | 10 | import os.path as osp | |
| 10 | 11 | import re | |
| 11 | 12 | import sys | |
@@ -15,6 +16,7 @@ | |||
| 15 | 16 | from gitdb import IStream | |
| 16 | 17 | ||
| 17 | 18 | from git import Actor, Commit, Repo | |
| 19 | + from git.exc import UnsafeOptionError | ||
| 18 | 20 | from git.objects.util import tzoffset, utc | |
| 19 | 21 | from git.repo.fun import touch | |
| 20 | 22 | ||
@@ -288,6 +290,17 @@ def test_iter_items(self): | |||
| 288 | 290 | # pretty not allowed. | |
| 289 | 291 | self.assertRaises(ValueError, Commit.iter_items, self.rorepo, "master", pretty="raw") | |
| 290 | 292 | ||
| 293 | + def test_iter_items_rejects_unsafe_revision(self): | ||
| 294 | + with tempfile.TemporaryDirectory() as tdir: | ||
| 295 | + marker = osp.join(tdir, "pwn") | ||
| 296 | + self.assertRaises(UnsafeOptionError, Commit.iter_items, self.rorepo, f"--output={marker}") | ||
| 297 | + | ||
| 298 | + def test_iter_items_rejects_unsafe_options(self): | ||
| 299 | + with tempfile.TemporaryDirectory() as tdir: | ||
| 300 | + marker = osp.join(tdir, "pwn") | ||
| 301 | + with self.assertRaises(UnsafeOptionError): | ||
| 302 | + list(Commit.iter_items(self.rorepo, "HEAD", output=marker)) | ||
| 303 | + | ||
| 291 | 304 | def test_rev_list_bisect_all(self): | |
| 292 | 305 | """ | |
| 293 | 306 | 'git rev-list --bisect-all' returns additional information | |
| Back | FazBrowse Home | New Git URL |
0 commit comments