| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent a54d159 commit 9ad28cc
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,6 +2,13 @@ | |||
| 2 | 2 | Changelog | |
| 3 | 3 | ========= | |
| 4 | 4 | ||
| 5 | + 3.2.1 | ||
| 6 | + ===== | ||
| 7 | + | ||
| 8 | + Security fixes for | ||
| 9 | + | ||
| 10 | + * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-w8jc-g24h-crhw | ||
| 11 | + | ||
| 5 | 12 | 3.2.0 | |
| 6 | 13 | ===== | |
| 7 | 14 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -645,7 +645,8 @@ class Git(metaclass=_GitMeta): | |||
| 645 | 645 | "_version_info_token", | |
| 646 | 646 | ) | |
| 647 | 647 | ||
| 648 | - re_unsafe_protocol = re.compile(r"(.+)::.+") | ||
| 648 | + # Match Git's leading transport selector, including an empty helper name. | ||
| 649 | + re_unsafe_protocol = re.compile(r"([A-Za-z0-9][A-Za-z0-9+.-]*|)::") | ||
| 649 | 650 | ||
| 650 | 651 | unsafe_git_ls_remote_options = [ | |
| 651 | 652 | # This option allows arbitrary command execution in git-ls-remote. | |
@@ -1129,16 +1130,28 @@ def ls_remote( | |||
| 1129 | 1130 | self, | |
| 1130 | 1131 | *args: Any, | |
| 1131 | 1132 | allow_unsafe_options: bool = False, | |
| 1133 | + allow_unsafe_protocols: bool = False, | ||
| 1132 | 1134 | **kwargs: Any, | |
| 1133 | 1135 | ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], "Git.AutoInterrupt"]: | |
| 1134 | 1136 | """List references in a remote repository. | |
| 1135 | 1137 | ||
| 1136 | 1138 | :param allow_unsafe_options: | |
| 1137 | 1139 | Allow unsafe options, like ``--upload-pack`` or ``--exec``. | |
| 1140 | + | ||
| 1141 | + :param allow_unsafe_protocols: | ||
| 1142 | + Allow unsafe protocols to be used, like ``ext``. Positional arguments | ||
| 1143 | + and split short-option values are checked. | ||
| 1138 | 1144 | """ | |
| 1139 | 1145 | if not allow_unsafe_options: | |
| 1140 | 1146 | candidate_options = self._option_candidates(args, kwargs) | |
| 1141 | 1147 | Git.check_unsafe_options(options=candidate_options, unsafe_options=self.unsafe_git_ls_remote_options) | |
| 1148 | + if not allow_unsafe_protocols: | ||
| 1149 | + protocol_args = list(args) | ||
| 1150 | + if kwargs.get("split_single_char_options", True): | ||
| 1151 | + # Split short-option values can become the URL after parsing earlier options. | ||
| 1152 | + protocol_args.extend(value for key, value in kwargs.items() if len(key) == 1) | ||
| 1153 | + for arg in self._unpack_args(protocol_args): | ||
| 1154 | + self.check_unsafe_protocols(arg) | ||
| 1142 | 1155 | return self._call_process("ls_remote", *args, **kwargs) | |
| 1143 | 1156 | ||
| 1144 | 1157 | @property | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1093,6 +1093,7 @@ def move( | |||
| 1093 | 1093 | self, | |
| 1094 | 1094 | items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]], | |
| 1095 | 1095 | skip_errors: bool = False, | |
| 1096 | + allow_unsafe_options: bool = False, | ||
| 1096 | 1097 | **kwargs: Any, | |
| 1097 | 1098 | ) -> List[Tuple[str, str]]: | |
| 1098 | 1099 | """Rename/move the items, whereas the last item is considered the destination of | |
@@ -1113,6 +1114,10 @@ def move( | |||
| 1113 | 1114 | If ``True``, errors such as ones resulting from missing source files will be | |
| 1114 | 1115 | skipped. | |
| 1115 | 1116 | ||
| 1117 | + :param allow_unsafe_options: | ||
| 1118 | + Allow unsafe options such as ``--pathspec-from-file`` to be passed to | ||
| 1119 | + :manpage:`git-mv(1)`. | ||
| 1120 | + | ||
| 1116 | 1121 | :param kwargs: | |
| 1117 | 1122 | Additional arguments you would like to pass to :manpage:`git-mv(1)`, such as | |
| 1118 | 1123 | ``dry_run`` or ``force``. | |
@@ -1129,6 +1134,11 @@ def move( | |||
| 1129 | 1134 | :raise git.exc.GitCommandError: | |
| 1130 | 1135 | If git could not handle your request. | |
| 1131 | 1136 | """ | |
| 1137 | + if not allow_unsafe_options: | ||
| 1138 | + Git.check_unsafe_options( | ||
| 1139 | + options=Git._option_candidates([], kwargs), | ||
| 1140 | + unsafe_options=Git.unsafe_git_pathspec_from_file_options, | ||
| 1141 | + ) | ||
| 1132 | 1142 | args = [] | |
| 1133 | 1143 | if skip_errors: | |
| 1134 | 1144 | args.append("-k") | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -892,14 +892,17 @@ def iter_commits( | |||
| 892 | 892 | **kwargs, | |
| 893 | 893 | ) | |
| 894 | 894 | ||
| 895 | - def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Commit]: | ||
| 895 | + def merge_base(self, *rev: TBD, allow_unsafe_options: bool = False, **kwargs: Any) -> List[Commit]: | ||
| 896 | 896 | R"""Find the closest common ancestor for the given revision | |
| 897 | 897 | (:class:`~git.objects.commit.Commit`\s, :class:`~git.refs.tag.Tag`\s, | |
| 898 | 898 | :class:`~git.refs.reference.Reference`\s, etc.). | |
| 899 | 899 | ||
| 900 | 900 | :param rev: | |
| 901 | 901 | At least two revs to find the common ancestor for. | |
| 902 | 902 | ||
| 903 | + :param allow_unsafe_options: | ||
| 904 | + Allow unsafe options in the revision arguments, like ``--output``. | ||
| 905 | + | ||
| 903 | 906 | :param kwargs: | |
| 904 | 907 | Additional arguments to be passed to the ``repo.git.merge_base()`` command | |
| 905 | 908 | which does all the work. | |
@@ -912,18 +915,25 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Commit]: | |||
| 912 | 915 | ||
| 913 | 916 | :raise ValueError: | |
| 914 | 917 | If fewer than two revisions are provided. | |
| 918 | + | ||
| 919 | + :raise git.exc.GitCommandError: | ||
| 920 | + If git fails for a reason other than having no common merge base. | ||
| 915 | 921 | """ | |
| 916 | 922 | if len(rev) < 2: | |
| 917 | 923 | raise ValueError("Please specify at least two revs, got only %i" % len(rev)) | |
| 918 | 924 | # END handle input | |
| 919 | 925 | ||
| 926 | + if not allow_unsafe_options: | ||
| 927 | + Git.check_unsafe_options( | ||
| 928 | + options=Git._option_candidates(rev, kwargs), unsafe_options=self.unsafe_git_revision_options | ||
| 929 | + ) | ||
| 930 | + | ||
| 920 | 931 | res: List[Commit] = [] | |
| 921 | 932 | try: | |
| 922 | 933 | lines: List[str] = self.git.merge_base(*rev, **kwargs).splitlines() | |
| 923 | 934 | except GitCommandError as err: | |
| 924 | - if err.status == 128: | ||
| 935 | + if err.status != 1: | ||
| 925 | 936 | raise | |
| 926 | - # END handle invalid rev | ||
| 927 | 937 | # Status code 1 is returned if there is no merge-base. | |
| 928 | 938 | # (See: https://github.com/git/git/blob/v2.44.0/builtin/merge-base.c#L19) | |
| 929 | 939 | return res | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,151 @@ | |||
| 1 | + """Command wrappers apply safety checks before starting Git.""" | ||
| 2 | + | ||
| 3 | + from pathlib import Path | ||
| 4 | + from unittest import mock | ||
| 5 | + | ||
| 6 | + import pytest | ||
| 7 | + | ||
| 8 | + from git import Actor, Git, GitCommandError, Repo | ||
| 9 | + from git.exc import UnsafeOptionError, UnsafeProtocolError | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + @pytest.mark.parametrize("allow_unsafe_options", [False, True]) | ||
| 13 | + @pytest.mark.parametrize( | ||
| 14 | + "args, kwargs", | ||
| 15 | + [ | ||
| 16 | + (("ext::helper",), {}), | ||
| 17 | + (("ext::",), {}), | ||
| 18 | + (("custom::address",), {}), | ||
| 19 | + (("1custom+v2.test-name::address",), {}), | ||
| 20 | + (("::address",), {}), | ||
| 21 | + (("custom::\naddress",), {}), | ||
| 22 | + ((["--refs", ("ext::helper",)],), {}), | ||
| 23 | + ((None, "--", "ext::helper", "HEAD"), {}), | ||
| 24 | + ((Path("ext::helper"),), {}), | ||
| 25 | + ((), {"q": "ext::helper"}), | ||
| 26 | + ((), {"h": ["ext::helper"]}), | ||
| 27 | + ((), {"-": "ext::helper"}), | ||
| 28 | + ((), {"o": [True, "ext::helper"]}), | ||
| 29 | + (("--server-option",), {"o": "ext::helper", "insert_kwargs_after": "--server-option"}), | ||
| 30 | + ], | ||
| 31 | + ) | ||
| 32 | + def test_ls_remote_rejects_unsafe_protocols(args, kwargs, allow_unsafe_options): | ||
| 33 | + with mock.patch.object(Git, "execute", side_effect=AssertionError("Git must not run")) as run: | ||
| 34 | + with pytest.raises(UnsafeProtocolError): | ||
| 35 | + Git().ls_remote(*args, allow_unsafe_options=allow_unsafe_options, **kwargs) | ||
| 36 | + run.assert_not_called() | ||
| 37 | + | ||
| 38 | + | ||
| 39 | + @pytest.mark.parametrize( | ||
| 40 | + "args, kwargs", | ||
| 41 | + [ | ||
| 42 | + ((), {}), | ||
| 43 | + ((None,), {}), | ||
| 44 | + (("origin", "HEAD"), {"h": True}), | ||
| 45 | + (("https://example.com/repo.git",), {}), | ||
| 46 | + (("git@example.com:repo.git",), {}), | ||
| 47 | + (("origin",), {"o": "key=value"}), | ||
| 48 | + (("origin",), {"server_option": "key::value"}), | ||
| 49 | + ], | ||
| 50 | + ) | ||
| 51 | + def test_ls_remote_preserves_safe_arguments(args, kwargs): | ||
| 52 | + with mock.patch.object(Git, "execute", return_value="refs") as run: | ||
| 53 | + assert Git().ls_remote(*args, **kwargs) == "refs" | ||
| 54 | + run.assert_called_once() | ||
| 55 | + | ||
| 56 | + | ||
| 57 | + @pytest.mark.parametrize( | ||
| 58 | + "url", | ||
| 59 | + [ | ||
| 60 | + "https://[::1]/repo.git", | ||
| 61 | + "ssh://git@[2001:db8::1]/repo.git", | ||
| 62 | + "https://example.com/repo::name", | ||
| 63 | + "git@example.com:repo::name", | ||
| 64 | + "./repo::name", | ||
| 65 | + ], | ||
| 66 | + ) | ||
| 67 | + def test_ls_remote_preserves_double_colons_outside_helper_selector(url): | ||
| 68 | + assert Git().ls_remote(url, get_url=True) == url | ||
| 69 | + | ||
| 70 | + | ||
| 71 | + def test_ls_remote_unsafe_opt_ins_are_independent(): | ||
| 72 | + with mock.patch.object(Git, "execute", return_value="refs") as run: | ||
| 73 | + with pytest.raises(UnsafeOptionError): | ||
| 74 | + Git().ls_remote("origin", upload_pack="helper", allow_unsafe_protocols=True) | ||
| 75 | + run.assert_not_called() | ||
| 76 | + assert ( | ||
| 77 | + Git().ls_remote("ext::helper", upload_pack="helper", allow_unsafe_protocols=True, allow_unsafe_options=True) | ||
| 78 | + == "refs" | ||
| 79 | + ) | ||
| 80 | + run.assert_called_once_with([Git.GIT_PYTHON_GIT_EXECUTABLE, "ls-remote", "--upload-pack=helper", "ext::helper"]) | ||
| 81 | + | ||
| 82 | + | ||
| 83 | + @pytest.mark.parametrize("allow_unsafe_options", [False, True]) | ||
| 84 | + @pytest.mark.parametrize( | ||
| 85 | + "revs, kwargs", | ||
| 86 | + [ | ||
| 87 | + (("HEAD", "--output=unused"), {}), | ||
| 88 | + (("HEAD", ["--out=unused"]), {}), | ||
| 89 | + (("HEAD", "-ounused"), {}), | ||
| 90 | + (("HEAD", "HEAD"), {"output": "unused"}), | ||
| 91 | + (("HEAD", "HEAD"), {"out": "unused"}), | ||
| 92 | + (("HEAD", "HEAD"), {"o": "unused"}), | ||
| 93 | + ], | ||
| 94 | + ) | ||
| 95 | + def test_merge_base_checks_unsafe_options(tmp_path, revs, kwargs, allow_unsafe_options): | ||
| 96 | + repo = Repo.init(tmp_path) | ||
| 97 | + with mock.patch.object(Git, "execute", return_value="") as run: | ||
| 98 | + if allow_unsafe_options: | ||
| 99 | + assert repo.merge_base(*revs, allow_unsafe_options=True, **kwargs) == [] | ||
| 100 | + run.assert_called_once() | ||
| 101 | + assert "--allow-unsafe-options" not in run.call_args[0][0] | ||
| 102 | + else: | ||
| 103 | + with pytest.raises(UnsafeOptionError): | ||
| 104 | + repo.merge_base(*revs, **kwargs) | ||
| 105 | + run.assert_not_called() | ||
| 106 | + | ||
| 107 | + | ||
| 108 | + @pytest.mark.parametrize("status", [-9, 2, 128, 129]) | ||
| 109 | + def test_merge_base_propagates_errors(tmp_path, status): | ||
| 110 | + repo = Repo.init(tmp_path) | ||
| 111 | + error = GitCommandError("git merge-base", status) | ||
| 112 | + with mock.patch.object(Git, "execute", side_effect=error): | ||
| 113 | + with pytest.raises(GitCommandError) as raised: | ||
| 114 | + repo.merge_base("HEAD", "HEAD") | ||
| 115 | + assert raised.value is error | ||
| 116 | + | ||
| 117 | + | ||
| 118 | + def test_merge_base_distinguishes_unrelated_history_from_invalid_options(tmp_path): | ||
| 119 | + repo = Repo.init(tmp_path) | ||
| 120 | + actor = Actor("Test", "test@example.com") | ||
| 121 | + first = repo.index.commit("first", author=actor, committer=actor) | ||
| 122 | + second = repo.index.commit("second", parent_commits=[], head=False, author=actor, committer=actor) | ||
| 123 | + assert repo.merge_base(first, first) == [first] | ||
| 124 | + assert repo.merge_base(first, second) == [] | ||
| 125 | + with pytest.raises(GitCommandError) as raised: | ||
| 126 | + repo.merge_base(first, second, invalid_option=True) | ||
| 127 | + assert raised.value.status == 129 | ||
| 128 | + | ||
| 129 | + | ||
| 130 | + @pytest.mark.parametrize("allow_unsafe_options", [False, True]) | ||
| 131 | + @pytest.mark.parametrize("option", ["pathspec_from_file", "pathspec-from-file", "pathspec_from"]) | ||
| 132 | + @pytest.mark.parametrize("dry_run", [False, True]) | ||
| 133 | + def test_move_checks_unsafe_options(tmp_path, option, dry_run, allow_unsafe_options): | ||
| 134 | + repo = Repo.init(tmp_path) | ||
| 135 | + with mock.patch.object(Git, "execute", return_value="Renaming source to destination\n") as run: | ||
| 136 | + kwargs = {option: "unused", "dry_run": dry_run} | ||
| 137 | + if allow_unsafe_options: | ||
| 138 | + assert repo.index.move(["source", "destination"], True, allow_unsafe_options=True, **kwargs) == [ | ||
| 139 | + ("source", "destination") | ||
| 140 | + ] | ||
| 141 | + assert run.call_count == (1 if dry_run else 2) | ||
| 142 | + for call in run.call_args_list: | ||
| 143 | + argv = call[0][0] | ||
| 144 | + assert "-k" in argv | ||
| 145 | + assert f"--{option.replace('_', '-')}=unused" in argv | ||
| 146 | + assert "--allow-unsafe-options" not in argv | ||
| 147 | + assert argv[-3:] == ["--", "source", "destination"] | ||
| 148 | + else: | ||
| 149 | + with pytest.raises(UnsafeOptionError): | ||
| 150 | + repo.index.move(["source", "destination"], **kwargs) | ||
| 151 | + run.assert_not_called() | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments