1*4882a593SmuzhiyunFrom fe9b71628767610a238e47cd46b82d411a7e871a Mon Sep 17 00:00:00 2001
2*4882a593SmuzhiyunFrom: Narpat Mali <narpat.mali@windriver.com>
3*4882a593SmuzhiyunDate: Sat, 7 Jan 2023 17:16:57 +0000
4*4882a593SmuzhiyunSubject: [PATCH] python3-git: CVE-2022-24439 fix from PR 1521
5*4882a593Smuzhiyun
6*4882a593SmuzhiyunForbid unsafe protocol URLs in Repo.clone{,_from}()
7*4882a593SmuzhiyunSince the URL is passed directly to git clone, and the remote-ext helper
8*4882a593Smuzhiyunwill happily execute shell commands, so by default disallow URLs that
9*4882a593Smuzhiyuncontain a "::" unless a new unsafe_protocols kwarg is passed.
10*4882a593Smuzhiyun(CVE-2022-24439)
11*4882a593Smuzhiyun
12*4882a593SmuzhiyunFixes #1515
13*4882a593Smuzhiyun
14*4882a593SmuzhiyunCVE: CVE-2022-24439
15*4882a593Smuzhiyun
16*4882a593SmuzhiyunUpstream-Status: Backport [https://github.com/gitpython-developers/GitPython/pull/1521]
17*4882a593Smuzhiyun
18*4882a593SmuzhiyunSigned-off-by: Narpat Mali <narpat.mali@windriver.com>
19*4882a593Smuzhiyun---
20*4882a593Smuzhiyun git/cmd.py                    | 51 ++++++++++++++++++++++++--
21*4882a593Smuzhiyun git/exc.py                    |  8 ++++
22*4882a593Smuzhiyun git/objects/submodule/base.py | 19 ++++++----
23*4882a593Smuzhiyun git/remote.py                 | 69 +++++++++++++++++++++++++++++++----
24*4882a593Smuzhiyun git/repo/base.py              | 44 ++++++++++++++++++----
25*4882a593Smuzhiyun 5 files changed, 166 insertions(+), 25 deletions(-)
26*4882a593Smuzhiyun
27*4882a593Smuzhiyundiff --git a/git/cmd.py b/git/cmd.py
28*4882a593Smuzhiyunindex 4f05698..77026d6 100644
29*4882a593Smuzhiyun--- a/git/cmd.py
30*4882a593Smuzhiyun+++ b/git/cmd.py
31*4882a593Smuzhiyun@@ -4,6 +4,7 @@
32*4882a593Smuzhiyun # This module is part of GitPython and is released under
33*4882a593Smuzhiyun # the BSD License: http://www.opensource.org/licenses/bsd-license.php
34*4882a593Smuzhiyun from __future__ import annotations
35*4882a593Smuzhiyun+import re
36*4882a593Smuzhiyun from contextlib import contextmanager
37*4882a593Smuzhiyun import io
38*4882a593Smuzhiyun import logging
39*4882a593Smuzhiyun@@ -31,7 +32,9 @@ from git.util import is_cygwin_git, cygpath, expand_path, remove_password_if_pre
40*4882a593Smuzhiyun
41*4882a593Smuzhiyun from .exc import (
42*4882a593Smuzhiyun     GitCommandError,
43*4882a593Smuzhiyun-    GitCommandNotFound
44*4882a593Smuzhiyun+    GitCommandNotFound,
45*4882a593Smuzhiyun+    UnsafeOptionError,
46*4882a593Smuzhiyun+    UnsafeProtocolError
47*4882a593Smuzhiyun )
48*4882a593Smuzhiyun from .util import (
49*4882a593Smuzhiyun     LazyMixin,
50*4882a593Smuzhiyun@@ -225,6 +228,8 @@ class Git(LazyMixin):
51*4882a593Smuzhiyun
52*4882a593Smuzhiyun     _excluded_ = ('cat_file_all', 'cat_file_header', '_version_info')
53*4882a593Smuzhiyun
54*4882a593Smuzhiyun+    re_unsafe_protocol = re.compile("(.+)::.+")
55*4882a593Smuzhiyun+
56*4882a593Smuzhiyun     def __getstate__(self) -> Dict[str, Any]:
57*4882a593Smuzhiyun         return slots_to_dict(self, exclude=self._excluded_)
58*4882a593Smuzhiyun
59*4882a593Smuzhiyun@@ -400,6 +405,44 @@ class Git(LazyMixin):
60*4882a593Smuzhiyun             url = url.replace("\\\\", "\\").replace("\\", "/")
61*4882a593Smuzhiyun         return url
62*4882a593Smuzhiyun
63*4882a593Smuzhiyun+    @classmethod
64*4882a593Smuzhiyun+    def check_unsafe_protocols(cls, url: str) -> None:
65*4882a593Smuzhiyun+        """
66*4882a593Smuzhiyun+        Check for unsafe protocols.
67*4882a593Smuzhiyun+        Apart from the usual protocols (http, git, ssh),
68*4882a593Smuzhiyun+        Git allows "remote helpers" that have the form `<transport>::<address>`,
69*4882a593Smuzhiyun+        one of these helpers (`ext::`) can be used to invoke any arbitrary command.
70*4882a593Smuzhiyun+        See:
71*4882a593Smuzhiyun+        - https://git-scm.com/docs/gitremote-helpers
72*4882a593Smuzhiyun+        - https://git-scm.com/docs/git-remote-ext
73*4882a593Smuzhiyun+        """
74*4882a593Smuzhiyun+        match = cls.re_unsafe_protocol.match(url)
75*4882a593Smuzhiyun+        if match:
76*4882a593Smuzhiyun+            protocol = match.group(1)
77*4882a593Smuzhiyun+            raise UnsafeProtocolError(
78*4882a593Smuzhiyun+                f"The `{protocol}::` protocol looks suspicious, use `allow_unsafe_protocols=True` to allow it."
79*4882a593Smuzhiyun+            )
80*4882a593Smuzhiyun+
81*4882a593Smuzhiyun+    @classmethod
82*4882a593Smuzhiyun+    def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None:
83*4882a593Smuzhiyun+        """
84*4882a593Smuzhiyun+        Check for unsafe options.
85*4882a593Smuzhiyun+        Some options that are passed to `git <command>` can be used to execute
86*4882a593Smuzhiyun+        arbitrary commands, this are blocked by default.
87*4882a593Smuzhiyun+        """
88*4882a593Smuzhiyun+        # Options can be of the form `foo` or `--foo bar` `--foo=bar`,
89*4882a593Smuzhiyun+        # so we need to check if they start with "--foo" or if they are equal to "foo".
90*4882a593Smuzhiyun+        bare_unsafe_options = [
91*4882a593Smuzhiyun+            option.lstrip("-")
92*4882a593Smuzhiyun+            for option in unsafe_options
93*4882a593Smuzhiyun+        ]
94*4882a593Smuzhiyun+        for option in options:
95*4882a593Smuzhiyun+            for unsafe_option, bare_option in zip(unsafe_options, bare_unsafe_options):
96*4882a593Smuzhiyun+                if option.startswith(unsafe_option) or option == bare_option:
97*4882a593Smuzhiyun+                    raise UnsafeOptionError(
98*4882a593Smuzhiyun+                        f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
99*4882a593Smuzhiyun+                    )
100*4882a593Smuzhiyun+
101*4882a593Smuzhiyun     class AutoInterrupt(object):
102*4882a593Smuzhiyun         """Kill/Interrupt the stored process instance once this instance goes out of scope. It is
103*4882a593Smuzhiyun         used to prevent processes piling up in case iterators stop reading.
104*4882a593Smuzhiyun@@ -1068,12 +1111,12 @@ class Git(LazyMixin):
105*4882a593Smuzhiyun         return args
106*4882a593Smuzhiyun
107*4882a593Smuzhiyun     @classmethod
108*4882a593Smuzhiyun-    def __unpack_args(cls, arg_list: Sequence[str]) -> List[str]:
109*4882a593Smuzhiyun+    def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]:
110*4882a593Smuzhiyun
111*4882a593Smuzhiyun         outlist = []
112*4882a593Smuzhiyun         if isinstance(arg_list, (list, tuple)):
113*4882a593Smuzhiyun             for arg in arg_list:
114*4882a593Smuzhiyun-                outlist.extend(cls.__unpack_args(arg))
115*4882a593Smuzhiyun+                outlist.extend(cls._unpack_args(arg))
116*4882a593Smuzhiyun         else:
117*4882a593Smuzhiyun             outlist.append(str(arg_list))
118*4882a593Smuzhiyun
119*4882a593Smuzhiyun@@ -1154,7 +1197,7 @@ class Git(LazyMixin):
120*4882a593Smuzhiyun         # Prepare the argument list
121*4882a593Smuzhiyun
122*4882a593Smuzhiyun         opt_args = self.transform_kwargs(**opts_kwargs)
123*4882a593Smuzhiyun-        ext_args = self.__unpack_args([a for a in args if a is not None])
124*4882a593Smuzhiyun+        ext_args = self._unpack_args([a for a in args if a is not None])
125*4882a593Smuzhiyun
126*4882a593Smuzhiyun         if insert_after_this_arg is None:
127*4882a593Smuzhiyun             args_list = opt_args + ext_args
128*4882a593Smuzhiyundiff --git a/git/exc.py b/git/exc.py
129*4882a593Smuzhiyunindex e8ff784..5c96db2 100644
130*4882a593Smuzhiyun--- a/git/exc.py
131*4882a593Smuzhiyun+++ b/git/exc.py
132*4882a593Smuzhiyun@@ -36,6 +36,14 @@ class NoSuchPathError(GitError, OSError):
133*4882a593Smuzhiyun     """ Thrown if a path could not be access by the system. """
134*4882a593Smuzhiyun
135*4882a593Smuzhiyun
136*4882a593Smuzhiyun+class UnsafeProtocolError(GitError):
137*4882a593Smuzhiyun+    """Thrown if unsafe protocols are passed without being explicitly allowed."""
138*4882a593Smuzhiyun+
139*4882a593Smuzhiyun+
140*4882a593Smuzhiyun+class UnsafeOptionError(GitError):
141*4882a593Smuzhiyun+    """Thrown if unsafe options are passed without being explicitly allowed."""
142*4882a593Smuzhiyun+
143*4882a593Smuzhiyun+
144*4882a593Smuzhiyun class CommandError(GitError):
145*4882a593Smuzhiyun     """Base class for exceptions thrown at every stage of `Popen()` execution.
146*4882a593Smuzhiyun
147*4882a593Smuzhiyundiff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
148*4882a593Smuzhiyunindex f782045..deb224e 100644
149*4882a593Smuzhiyun--- a/git/objects/submodule/base.py
150*4882a593Smuzhiyun+++ b/git/objects/submodule/base.py
151*4882a593Smuzhiyun@@ -264,7 +264,8 @@ class Submodule(IndexObject, TraversableIterableObj):
152*4882a593Smuzhiyun         # end
153*4882a593Smuzhiyun
154*4882a593Smuzhiyun     @classmethod
155*4882a593Smuzhiyun-    def _clone_repo(cls, repo: 'Repo', url: str, path: PathLike, name: str, **kwargs: Any) -> 'Repo':
156*4882a593Smuzhiyun+    def _clone_repo(cls, repo: 'Repo', url: str, path: PathLike, name: str,
157*4882a593Smuzhiyun+            allow_unsafe_options: bool = False, allow_unsafe_protocols: bool = False,**kwargs: Any) -> 'Repo':
158*4882a593Smuzhiyun         """:return: Repo instance of newly cloned repository
159*4882a593Smuzhiyun         :param repo: our parent repository
160*4882a593Smuzhiyun         :param url: url to clone from
161*4882a593Smuzhiyun@@ -281,7 +282,8 @@ class Submodule(IndexObject, TraversableIterableObj):
162*4882a593Smuzhiyun             module_checkout_path = osp.join(str(repo.working_tree_dir), path)
163*4882a593Smuzhiyun         # end
164*4882a593Smuzhiyun
165*4882a593Smuzhiyun-        clone = git.Repo.clone_from(url, module_checkout_path, **kwargs)
166*4882a593Smuzhiyun+        clone = git.Repo.clone_from(url, module_checkout_path, allow_unsafe_options=allow_unsafe_options,
167*4882a593Smuzhiyun+                allow_unsafe_protocols=allow_unsafe_protocols, **kwargs)
168*4882a593Smuzhiyun         if cls._need_gitfile_submodules(repo.git):
169*4882a593Smuzhiyun             cls._write_git_file_and_module_config(module_checkout_path, module_abspath)
170*4882a593Smuzhiyun         # end
171*4882a593Smuzhiyun@@ -338,8 +340,8 @@ class Submodule(IndexObject, TraversableIterableObj):
172*4882a593Smuzhiyun     @classmethod
173*4882a593Smuzhiyun     def add(cls, repo: 'Repo', name: str, path: PathLike, url: Union[str, None] = None,
174*4882a593Smuzhiyun             branch: Union[str, None] = None, no_checkout: bool = False, depth: Union[int, None] = None,
175*4882a593Smuzhiyun-            env: Union[Mapping[str, str], None] = None, clone_multi_options: Union[Sequence[TBD], None] = None
176*4882a593Smuzhiyun-            ) -> 'Submodule':
177*4882a593Smuzhiyun+            env: Union[Mapping[str, str], None] = None, clone_multi_options: Union[Sequence[TBD], None] = None,
178*4882a593Smuzhiyun+            allow_unsafe_options: bool = False, allow_unsafe_protocols: bool = False,) -> 'Submodule':
179*4882a593Smuzhiyun         """Add a new submodule to the given repository. This will alter the index
180*4882a593Smuzhiyun         as well as the .gitmodules file, but will not create a new commit.
181*4882a593Smuzhiyun         If the submodule already exists, no matter if the configuration differs
182*4882a593Smuzhiyun@@ -447,7 +449,8 @@ class Submodule(IndexObject, TraversableIterableObj):
183*4882a593Smuzhiyun                 kwargs['multi_options'] = clone_multi_options
184*4882a593Smuzhiyun
185*4882a593Smuzhiyun             # _clone_repo(cls, repo, url, path, name, **kwargs):
186*4882a593Smuzhiyun-            mrepo = cls._clone_repo(repo, url, path, name, env=env, **kwargs)
187*4882a593Smuzhiyun+            mrepo = cls._clone_repo(repo, url, path, name, env=env, allow_unsafe_options=allow_unsafe_options,
188*4882a593Smuzhiyun+                allow_unsafe_protocols=allow_unsafe_protocols, **kwargs)
189*4882a593Smuzhiyun         # END verify url
190*4882a593Smuzhiyun
191*4882a593Smuzhiyun         ## See #525 for ensuring git urls in config-files valid under Windows.
192*4882a593Smuzhiyun@@ -484,7 +487,8 @@ class Submodule(IndexObject, TraversableIterableObj):
193*4882a593Smuzhiyun     def update(self, recursive: bool = False, init: bool = True, to_latest_revision: bool = False,
194*4882a593Smuzhiyun                progress: Union['UpdateProgress', None] = None, dry_run: bool = False,
195*4882a593Smuzhiyun                force: bool = False, keep_going: bool = False, env: Union[Mapping[str, str], None] = None,
196*4882a593Smuzhiyun-               clone_multi_options: Union[Sequence[TBD], None] = None) -> 'Submodule':
197*4882a593Smuzhiyun+               clone_multi_options: Union[Sequence[TBD], None] = None, allow_unsafe_options: bool = False,
198*4882a593Smuzhiyun+               allow_unsafe_protocols: bool = False) -> 'Submodule':
199*4882a593Smuzhiyun         """Update the repository of this submodule to point to the checkout
200*4882a593Smuzhiyun         we point at with the binsha of this instance.
201*4882a593Smuzhiyun
202*4882a593Smuzhiyun@@ -585,7 +589,8 @@ class Submodule(IndexObject, TraversableIterableObj):
203*4882a593Smuzhiyun                                 (self.url, checkout_module_abspath, self.name))
204*4882a593Smuzhiyun                 if not dry_run:
205*4882a593Smuzhiyun                     mrepo = self._clone_repo(self.repo, self.url, self.path, self.name, n=True, env=env,
206*4882a593Smuzhiyun-                                             multi_options=clone_multi_options)
207*4882a593Smuzhiyun+                            multi_options=clone_multi_options, allow_unsafe_options=allow_unsafe_options,
208*4882a593Smuzhiyun+                            allow_unsafe_protocols=allow_unsafe_protocols)
209*4882a593Smuzhiyun                 # END handle dry-run
210*4882a593Smuzhiyun                 progress.update(END | CLONE, 0, 1, prefix + "Done cloning to %s" % checkout_module_abspath)
211*4882a593Smuzhiyun
212*4882a593Smuzhiyundiff --git a/git/remote.py b/git/remote.py
213*4882a593Smuzhiyunindex 59681bc..cea6b99 100644
214*4882a593Smuzhiyun--- a/git/remote.py
215*4882a593Smuzhiyun+++ b/git/remote.py
216*4882a593Smuzhiyun@@ -473,6 +473,23 @@ class Remote(LazyMixin, IterableObj):
217*4882a593Smuzhiyun     __slots__ = ("repo", "name", "_config_reader")
218*4882a593Smuzhiyun     _id_attribute_ = "name"
219*4882a593Smuzhiyun
220*4882a593Smuzhiyun+    unsafe_git_fetch_options = [
221*4882a593Smuzhiyun+        # This option allows users to execute arbitrary commands.
222*4882a593Smuzhiyun+        # https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt---upload-packltupload-packgt
223*4882a593Smuzhiyun+        "--upload-pack",
224*4882a593Smuzhiyun+    ]
225*4882a593Smuzhiyun+    unsafe_git_pull_options = [
226*4882a593Smuzhiyun+        # This option allows users to execute arbitrary commands.
227*4882a593Smuzhiyun+        # https://git-scm.com/docs/git-pull#Documentation/git-pull.txt---upload-packltupload-packgt
228*4882a593Smuzhiyun+        "--upload-pack"
229*4882a593Smuzhiyun+    ]
230*4882a593Smuzhiyun+    unsafe_git_push_options = [
231*4882a593Smuzhiyun+        # This option allows users to execute arbitrary commands.
232*4882a593Smuzhiyun+        # https://git-scm.com/docs/git-push#Documentation/git-push.txt---execltgit-receive-packgt
233*4882a593Smuzhiyun+        "--receive-pack",
234*4882a593Smuzhiyun+        "--exec",
235*4882a593Smuzhiyun+    ]
236*4882a593Smuzhiyun+
237*4882a593Smuzhiyun     def __init__(self, repo: 'Repo', name: str) -> None:
238*4882a593Smuzhiyun         """Initialize a remote instance
239*4882a593Smuzhiyun
240*4882a593Smuzhiyun@@ -549,7 +566,8 @@ class Remote(LazyMixin, IterableObj):
241*4882a593Smuzhiyun             yield Remote(repo, section[lbound + 1:rbound])
242*4882a593Smuzhiyun         # END for each configuration section
243*4882a593Smuzhiyun
244*4882a593Smuzhiyun-    def set_url(self, new_url: str, old_url: Optional[str] = None, **kwargs: Any) -> 'Remote':
245*4882a593Smuzhiyun+    def set_url(self, new_url: str, old_url: Optional[str] = None,
246*4882a593Smuzhiyun+            allow_unsafe_protocols: bool = False, **kwargs: Any) -> 'Remote':
247*4882a593Smuzhiyun         """Configure URLs on current remote (cf command git remote set_url)
248*4882a593Smuzhiyun
249*4882a593Smuzhiyun         This command manages URLs on the remote.
250*4882a593Smuzhiyun@@ -558,15 +576,17 @@ class Remote(LazyMixin, IterableObj):
251*4882a593Smuzhiyun         :param old_url: when set, replaces this URL with new_url for the remote
252*4882a593Smuzhiyun         :return: self
253*4882a593Smuzhiyun         """
254*4882a593Smuzhiyun+        if not allow_unsafe_protocols:
255*4882a593Smuzhiyun+            Git.check_unsafe_protocols(new_url)
256*4882a593Smuzhiyun         scmd = 'set-url'
257*4882a593Smuzhiyun         kwargs['insert_kwargs_after'] = scmd
258*4882a593Smuzhiyun         if old_url:
259*4882a593Smuzhiyun-            self.repo.git.remote(scmd, self.name, new_url, old_url, **kwargs)
260*4882a593Smuzhiyun+            self.repo.git.remote(scmd, "--", self.name, new_url, old_url, **kwargs)
261*4882a593Smuzhiyun         else:
262*4882a593Smuzhiyun-            self.repo.git.remote(scmd, self.name, new_url, **kwargs)
263*4882a593Smuzhiyun+            self.repo.git.remote(scmd, "--", self.name, new_url, **kwargs)
264*4882a593Smuzhiyun         return self
265*4882a593Smuzhiyun
266*4882a593Smuzhiyun-    def add_url(self, url: str, **kwargs: Any) -> 'Remote':
267*4882a593Smuzhiyun+    def add_url(self, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> 'Remote':
268*4882a593Smuzhiyun         """Adds a new url on current remote (special case of git remote set_url)
269*4882a593Smuzhiyun
270*4882a593Smuzhiyun         This command adds new URLs to a given remote, making it possible to have
271*4882a593Smuzhiyun@@ -575,7 +595,7 @@ class Remote(LazyMixin, IterableObj):
272*4882a593Smuzhiyun         :param url: string being the URL to add as an extra remote URL
273*4882a593Smuzhiyun         :return: self
274*4882a593Smuzhiyun         """
275*4882a593Smuzhiyun-        return self.set_url(url, add=True)
276*4882a593Smuzhiyun+        return self.set_url(url, add=True, allow_unsafe_protocols=allow_unsafe_protocols)
277*4882a593Smuzhiyun
278*4882a593Smuzhiyun     def delete_url(self, url: str, **kwargs: Any) -> 'Remote':
279*4882a593Smuzhiyun         """Deletes a new url on current remote (special case of git remote set_url)
280*4882a593Smuzhiyun@@ -667,7 +687,7 @@ class Remote(LazyMixin, IterableObj):
281*4882a593Smuzhiyun         return out_refs
282*4882a593Smuzhiyun
283*4882a593Smuzhiyun     @ classmethod
284*4882a593Smuzhiyun-    def create(cls, repo: 'Repo', name: str, url: str, **kwargs: Any) -> 'Remote':
285*4882a593Smuzhiyun+    def create(cls, repo: 'Repo', name: str, url: str, allow_unsafe_protocols: bool = False, *kwargs: Any) -> 'Remote':
286*4882a593Smuzhiyun         """Create a new remote to the given repository
287*4882a593Smuzhiyun         :param repo: Repository instance that is to receive the new remote
288*4882a593Smuzhiyun         :param name: Desired name of the remote
289*4882a593Smuzhiyun@@ -677,7 +697,10 @@ class Remote(LazyMixin, IterableObj):
290*4882a593Smuzhiyun         :raise GitCommandError: in case an origin with that name already exists"""
291*4882a593Smuzhiyun         scmd = 'add'
292*4882a593Smuzhiyun         kwargs['insert_kwargs_after'] = scmd
293*4882a593Smuzhiyun-        repo.git.remote(scmd, name, Git.polish_url(url), **kwargs)
294*4882a593Smuzhiyun+        url = Git.polish_url(url)
295*4882a593Smuzhiyun+        if not allow_unsafe_protocols:
296*4882a593Smuzhiyun+            Git.check_unsafe_protocols(url)
297*4882a593Smuzhiyun+        repo.git.remote(scmd, "--", name, url, **kwargs)
298*4882a593Smuzhiyun         return cls(repo, name)
299*4882a593Smuzhiyun
300*4882a593Smuzhiyun     # add is an alias
301*4882a593Smuzhiyun@@ -840,6 +863,8 @@ class Remote(LazyMixin, IterableObj):
302*4882a593Smuzhiyun               progress: Union[RemoteProgress, None, 'UpdateProgress'] = None,
303*4882a593Smuzhiyun               verbose: bool = True,
304*4882a593Smuzhiyun               kill_after_timeout: Union[None, float] = None,
305*4882a593Smuzhiyun+              allow_unsafe_protocols: bool = False,
306*4882a593Smuzhiyun+              allow_unsafe_options: bool = False,
307*4882a593Smuzhiyun               **kwargs: Any) -> IterableList[FetchInfo]:
308*4882a593Smuzhiyun         """Fetch the latest changes for this remote
309*4882a593Smuzhiyun
310*4882a593Smuzhiyun@@ -881,6 +906,14 @@ class Remote(LazyMixin, IterableObj):
311*4882a593Smuzhiyun         else:
312*4882a593Smuzhiyun             args = [refspec]
313*4882a593Smuzhiyun
314*4882a593Smuzhiyun+        if not allow_unsafe_protocols:
315*4882a593Smuzhiyun+            for ref in args:
316*4882a593Smuzhiyun+                if ref:
317*4882a593Smuzhiyun+                    Git.check_unsafe_protocols(ref)
318*4882a593Smuzhiyun+
319*4882a593Smuzhiyun+        if not allow_unsafe_options:
320*4882a593Smuzhiyun+            Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_fetch_options)
321*4882a593Smuzhiyun+
322*4882a593Smuzhiyun         proc = self.repo.git.fetch("--", self, *args, as_process=True, with_stdout=False,
323*4882a593Smuzhiyun                                    universal_newlines=True, v=verbose, **kwargs)
324*4882a593Smuzhiyun         res = self._get_fetch_info_from_stderr(proc, progress,
325*4882a593Smuzhiyun@@ -892,6 +925,8 @@ class Remote(LazyMixin, IterableObj):
326*4882a593Smuzhiyun     def pull(self, refspec: Union[str, List[str], None] = None,
327*4882a593Smuzhiyun              progress: Union[RemoteProgress, 'UpdateProgress', None] = None,
328*4882a593Smuzhiyun              kill_after_timeout: Union[None, float] = None,
329*4882a593Smuzhiyun+             allow_unsafe_protocols: bool = False,
330*4882a593Smuzhiyun+             allow_unsafe_options: bool = False,
331*4882a593Smuzhiyun              **kwargs: Any) -> IterableList[FetchInfo]:
332*4882a593Smuzhiyun         """Pull changes from the given branch, being the same as a fetch followed
333*4882a593Smuzhiyun         by a merge of branch with your local branch.
334*4882a593Smuzhiyun@@ -905,6 +940,15 @@ class Remote(LazyMixin, IterableObj):
335*4882a593Smuzhiyun             # No argument refspec, then ensure the repo's config has a fetch refspec.
336*4882a593Smuzhiyun             self._assert_refspec()
337*4882a593Smuzhiyun         kwargs = add_progress(kwargs, self.repo.git, progress)
338*4882a593Smuzhiyun+
339*4882a593Smuzhiyun+        refspec = Git._unpack_args(refspec or [])
340*4882a593Smuzhiyun+        if not allow_unsafe_protocols:
341*4882a593Smuzhiyun+            for ref in refspec:
342*4882a593Smuzhiyun+                Git.check_unsafe_protocols(ref)
343*4882a593Smuzhiyun+
344*4882a593Smuzhiyun+        if not allow_unsafe_options:
345*4882a593Smuzhiyun+            Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_pull_options)
346*4882a593Smuzhiyun+
347*4882a593Smuzhiyun         proc = self.repo.git.pull("--", self, refspec, with_stdout=False, as_process=True,
348*4882a593Smuzhiyun                                   universal_newlines=True, v=True, **kwargs)
349*4882a593Smuzhiyun         res = self._get_fetch_info_from_stderr(proc, progress,
350*4882a593Smuzhiyun@@ -916,6 +960,8 @@ class Remote(LazyMixin, IterableObj):
351*4882a593Smuzhiyun     def push(self, refspec: Union[str, List[str], None] = None,
352*4882a593Smuzhiyun              progress: Union[RemoteProgress, 'UpdateProgress', Callable[..., RemoteProgress], None] = None,
353*4882a593Smuzhiyun              kill_after_timeout: Union[None, float] = None,
354*4882a593Smuzhiyun+             allow_unsafe_protocols: bool = False,
355*4882a593Smuzhiyun+             allow_unsafe_options: bool = False,
356*4882a593Smuzhiyun              **kwargs: Any) -> IterableList[PushInfo]:
357*4882a593Smuzhiyun         """Push changes from source branch in refspec to target branch in refspec.
358*4882a593Smuzhiyun
359*4882a593Smuzhiyun@@ -945,6 +991,15 @@ class Remote(LazyMixin, IterableObj):
360*4882a593Smuzhiyun             If the operation fails completely, the length of the returned IterableList will
361*4882a593Smuzhiyun             be 0."""
362*4882a593Smuzhiyun         kwargs = add_progress(kwargs, self.repo.git, progress)
363*4882a593Smuzhiyun+
364*4882a593Smuzhiyun+        refspec = Git._unpack_args(refspec or [])
365*4882a593Smuzhiyun+        if not allow_unsafe_protocols:
366*4882a593Smuzhiyun+            for ref in refspec:
367*4882a593Smuzhiyun+                Git.check_unsafe_protocols(ref)
368*4882a593Smuzhiyun+
369*4882a593Smuzhiyun+        if not allow_unsafe_options:
370*4882a593Smuzhiyun+            Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=self.unsafe_git_push_options)
371*4882a593Smuzhiyun+
372*4882a593Smuzhiyun         proc = self.repo.git.push("--", self, refspec, porcelain=True, as_process=True,
373*4882a593Smuzhiyun                                   universal_newlines=True,
374*4882a593Smuzhiyun                                   kill_after_timeout=kill_after_timeout,
375*4882a593Smuzhiyundiff --git a/git/repo/base.py b/git/repo/base.py
376*4882a593Smuzhiyunindex f14f929..7b3565b 100644
377*4882a593Smuzhiyun--- a/git/repo/base.py
378*4882a593Smuzhiyun+++ b/git/repo/base.py
379*4882a593Smuzhiyun@@ -24,7 +24,11 @@ from git.compat import (
380*4882a593Smuzhiyun )
381*4882a593Smuzhiyun from git.config import GitConfigParser
382*4882a593Smuzhiyun from git.db import GitCmdObjectDB
383*4882a593Smuzhiyun-from git.exc import InvalidGitRepositoryError, NoSuchPathError, GitCommandError
384*4882a593Smuzhiyun+from git.exc import (
385*4882a593Smuzhiyun+    GitCommandError,
386*4882a593Smuzhiyun+    InvalidGitRepositoryError,
387*4882a593Smuzhiyun+    NoSuchPathError,
388*4882a593Smuzhiyun+)
389*4882a593Smuzhiyun from git.index import IndexFile
390*4882a593Smuzhiyun from git.objects import Submodule, RootModule, Commit
391*4882a593Smuzhiyun from git.refs import HEAD, Head, Reference, TagReference
392*4882a593Smuzhiyun@@ -97,6 +101,18 @@ class Repo(object):
393*4882a593Smuzhiyun     re_author_committer_start = re.compile(r'^(author|committer)')
394*4882a593Smuzhiyun     re_tab_full_line = re.compile(r'^\t(.*)$')
395*4882a593Smuzhiyun
396*4882a593Smuzhiyun+    unsafe_git_clone_options = [
397*4882a593Smuzhiyun+        # This option allows users to execute arbitrary commands.
398*4882a593Smuzhiyun+        # https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---upload-packltupload-packgt
399*4882a593Smuzhiyun+        "--upload-pack",
400*4882a593Smuzhiyun+        "-u",
401*4882a593Smuzhiyun+        # Users can override configuration variables
402*4882a593Smuzhiyun+        # like `protocol.allow` or `core.gitProxy` to execute arbitrary commands.
403*4882a593Smuzhiyun+        # https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---configltkeygtltvaluegt
404*4882a593Smuzhiyun+        "--config",
405*4882a593Smuzhiyun+        "-c",
406*4882a593Smuzhiyun+    ]
407*4882a593Smuzhiyun+
408*4882a593Smuzhiyun     # invariants
409*4882a593Smuzhiyun     # represents the configuration level of a configuration file
410*4882a593Smuzhiyun     config_level: ConfigLevels_Tup = ("system", "user", "global", "repository")
411*4882a593Smuzhiyun@@ -1049,7 +1065,8 @@ class Repo(object):
412*4882a593Smuzhiyun     @ classmethod
413*4882a593Smuzhiyun     def _clone(cls, git: 'Git', url: PathLike, path: PathLike, odb_default_type: Type[GitCmdObjectDB],
414*4882a593Smuzhiyun                progress: Union['RemoteProgress', 'UpdateProgress', Callable[..., 'RemoteProgress'], None] = None,
415*4882a593Smuzhiyun-               multi_options: Optional[List[str]] = None, **kwargs: Any
416*4882a593Smuzhiyun+               multi_options: Optional[List[str]] = None, allow_unsafe_protocols: bool = False,
417*4882a593Smuzhiyun+               allow_unsafe_options: bool = False, **kwargs: Any
418*4882a593Smuzhiyun                ) -> 'Repo':
419*4882a593Smuzhiyun         odbt = kwargs.pop('odbt', odb_default_type)
420*4882a593Smuzhiyun
421*4882a593Smuzhiyun@@ -1072,6 +1089,12 @@ class Repo(object):
422*4882a593Smuzhiyun         multi = None
423*4882a593Smuzhiyun         if multi_options:
424*4882a593Smuzhiyun             multi = shlex.split(' '.join(multi_options))
425*4882a593Smuzhiyun+
426*4882a593Smuzhiyun+        if not allow_unsafe_protocols:
427*4882a593Smuzhiyun+            Git.check_unsafe_protocols(str(url))
428*4882a593Smuzhiyun+        if not allow_unsafe_options and multi_options:
429*4882a593Smuzhiyun+            Git.check_unsafe_options(options=multi_options, unsafe_options=cls.unsafe_git_clone_options)
430*4882a593Smuzhiyun+
431*4882a593Smuzhiyun         proc = git.clone("--", multi, Git.polish_url(str(url)), clone_path, with_extended_output=True, as_process=True,
432*4882a593Smuzhiyun                          v=True, universal_newlines=True, **add_progress(kwargs, git, progress))
433*4882a593Smuzhiyun         if progress:
434*4882a593Smuzhiyun@@ -1107,7 +1130,9 @@ class Repo(object):
435*4882a593Smuzhiyun         return repo
436*4882a593Smuzhiyun
437*4882a593Smuzhiyun     def clone(self, path: PathLike, progress: Optional[Callable] = None,
438*4882a593Smuzhiyun-              multi_options: Optional[List[str]] = None, **kwargs: Any) -> 'Repo':
439*4882a593Smuzhiyun+              multi_options: Optional[List[str]] = None, unsafe_protocols: bool = False,
440*4882a593Smuzhiyun+              allow_unsafe_protocols: bool = False, allow_unsafe_options: bool = False,
441*4882a593Smuzhiyun+              **kwargs: Any) -> 'Repo':
442*4882a593Smuzhiyun         """Create a clone from this repository.
443*4882a593Smuzhiyun
444*4882a593Smuzhiyun         :param path: is the full path of the new repo (traditionally ends with ./<name>.git).
445*4882a593Smuzhiyun@@ -1116,18 +1141,21 @@ class Repo(object):
446*4882a593Smuzhiyun             option per list item which is passed exactly as specified to clone.
447*4882a593Smuzhiyun             For example ['--config core.filemode=false', '--config core.ignorecase',
448*4882a593Smuzhiyun             '--recurse-submodule=repo1_path', '--recurse-submodule=repo2_path']
449*4882a593Smuzhiyun+        :param unsafe_protocols: Allow unsafe protocols to be used, like ex
450*4882a593Smuzhiyun         :param kwargs:
451*4882a593Smuzhiyun             * odbt = ObjectDatabase Type, allowing to determine the object database
452*4882a593Smuzhiyun               implementation used by the returned Repo instance
453*4882a593Smuzhiyun             * All remaining keyword arguments are given to the git-clone command
454*4882a593Smuzhiyun
455*4882a593Smuzhiyun         :return: ``git.Repo`` (the newly cloned repo)"""
456*4882a593Smuzhiyun-        return self._clone(self.git, self.common_dir, path, type(self.odb), progress, multi_options, **kwargs)
457*4882a593Smuzhiyun+        return self._clone(self.git, self.common_dir, path, type(self.odb), progress, multi_options,
458*4882a593Smuzhiyun+                allow_unsafe_protocols=allow_unsafe_protocols, allow_unsafe_options=allow_unsafe_options, **kwargs)
459*4882a593Smuzhiyun
460*4882a593Smuzhiyun     @ classmethod
461*4882a593Smuzhiyun     def clone_from(cls, url: PathLike, to_path: PathLike, progress: Optional[Callable] = None,
462*4882a593Smuzhiyun-                   env: Optional[Mapping[str, str]] = None,
463*4882a593Smuzhiyun-                   multi_options: Optional[List[str]] = None, **kwargs: Any) -> 'Repo':
464*4882a593Smuzhiyun+                   env: Optional[Mapping[str, str]] = None, multi_options: Optional[List[str]] = None,
465*4882a593Smuzhiyun+                   unsafe_protocols: bool = False, allow_unsafe_protocols: bool = False,
466*4882a593Smuzhiyun+                   allow_unsafe_options: bool = False, **kwargs: Any) -> 'Repo':
467*4882a593Smuzhiyun         """Create a clone from the given URL
468*4882a593Smuzhiyun
469*4882a593Smuzhiyun         :param url: valid git url, see http://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS
470*4882a593Smuzhiyun@@ -1140,12 +1168,14 @@ class Repo(object):
471*4882a593Smuzhiyun             If you want to unset some variable, consider providing empty string
472*4882a593Smuzhiyun             as its value.
473*4882a593Smuzhiyun         :param multi_options: See ``clone`` method
474*4882a593Smuzhiyun+        :param unsafe_protocols: Allow unsafe protocols to be used, like ext
475*4882a593Smuzhiyun         :param kwargs: see the ``clone`` method
476*4882a593Smuzhiyun         :return: Repo instance pointing to the cloned directory"""
477*4882a593Smuzhiyun         git = cls.GitCommandWrapperType(os.getcwd())
478*4882a593Smuzhiyun         if env is not None:
479*4882a593Smuzhiyun             git.update_environment(**env)
480*4882a593Smuzhiyun-        return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs)
481*4882a593Smuzhiyun+        return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options,
482*4882a593Smuzhiyun+                allow_unsafe_protocols=allow_unsafe_protocols, allow_unsafe_options=allow_unsafe_options, **kwargs)
483*4882a593Smuzhiyun
484*4882a593Smuzhiyun     def archive(self, ostream: Union[TextIO, BinaryIO], treeish: Optional[str] = None,
485*4882a593Smuzhiyun                 prefix: Optional[str] = None, **kwargs: Any) -> Repo:
486*4882a593Smuzhiyun--
487*4882a593Smuzhiyun2.34.1
488*4882a593Smuzhiyun
489