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