From 2625ed9fc074091c531c27ffcba7902771130261 Mon Sep 17 00:00:00 2001 From: Steve Kowalik Date: Tue, 20 Dec 2022 17:05:50 +1100 Subject: Forbid unsafe protocol URLs in Repo.clone{,_from}() Since the URL is passed directly to git clone, and the remote-ext helper will happily execute shell commands, so by default disallow URLs that contain a "::" unless a new unsafe_protocols kwarg is passed. (CVE-2022-24439) Fixes #1515 --- git/repo/base.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) (limited to 'git/repo/base.py') diff --git a/git/repo/base.py b/git/repo/base.py index 49a3d5a1..35ff68b0 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -21,7 +21,12 @@ from git.compat import ( ) from git.config import GitConfigParser from git.db import GitCmdObjectDB -from git.exc import InvalidGitRepositoryError, NoSuchPathError, GitCommandError +from git.exc import ( + GitCommandError, + InvalidGitRepositoryError, + NoSuchPathError, + UnsafeOptionsUsedError, +) from git.index import IndexFile from git.objects import Submodule, RootModule, Commit from git.refs import HEAD, Head, Reference, TagReference @@ -128,6 +133,7 @@ class Repo(object): re_envvars = re.compile(r"(\$(\{\s?)?[a-zA-Z_]\w*(\}\s?)?|%\s?[a-zA-Z_]\w*\s?%)") re_author_committer_start = re.compile(r"^(author|committer)") re_tab_full_line = re.compile(r"^\t(.*)$") + re_config_protocol_option = re.compile(r"-[-]?c(|onfig)\s+protocol\.", re.I) # invariants # represents the configuration level of a configuration file @@ -1215,11 +1221,27 @@ class Repo(object): # END handle remote repo return repo + @classmethod + def unsafe_options( + cls, + url: str, + multi_options: Optional[List[str]] = None, + ) -> bool: + if "ext::" in url: + return True + if multi_options is not None: + if any(["--upload-pack" in m for m in multi_options]): + return True + if any([re.match(cls.re_config_protocol_option, m) for m in multi_options]): + return True + return False + def clone( self, path: PathLike, progress: Optional[Callable] = None, multi_options: Optional[List[str]] = None, + unsafe_protocols: bool = False, **kwargs: Any, ) -> "Repo": """Create a clone from this repository. @@ -1230,12 +1252,15 @@ class Repo(object): option per list item which is passed exactly as specified to clone. For example ['--config core.filemode=false', '--config core.ignorecase', '--recurse-submodule=repo1_path', '--recurse-submodule=repo2_path'] + :param unsafe_protocols: Allow unsafe protocols to be used, like ext :param kwargs: * odbt = ObjectDatabase Type, allowing to determine the object database implementation used by the returned Repo instance * All remaining keyword arguments are given to the git-clone command :return: ``git.Repo`` (the newly cloned repo)""" + if not unsafe_protocols and self.unsafe_options(path, multi_options): + raise UnsafeOptionsUsedError(f"{path} requires unsafe_protocols flag") return self._clone( self.git, self.common_dir, @@ -1254,6 +1279,7 @@ class Repo(object): progress: Optional[Callable] = None, env: Optional[Mapping[str, str]] = None, multi_options: Optional[List[str]] = None, + unsafe_protocols: bool = False, **kwargs: Any, ) -> "Repo": """Create a clone from the given URL @@ -1268,11 +1294,14 @@ class Repo(object): If you want to unset some variable, consider providing empty string as its value. :param multi_options: See ``clone`` method + :param unsafe_protocols: Allow unsafe protocols to be used, like ext :param kwargs: see the ``clone`` method :return: Repo instance pointing to the cloned directory""" git = cls.GitCommandWrapperType(os.getcwd()) if env is not None: git.update_environment(**env) + if not unsafe_protocols and cls.unsafe_options(url, multi_options): + raise UnsafeOptionsUsedError(f"{url} requires unsafe_protocols flag") return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs) def archive( -- cgit v1.2.1 From e6108c7997f5c8f7361b982959518e982b973230 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Fri, 23 Dec 2022 20:19:52 -0500 Subject: Block unsafe options and protocols by default --- git/repo/base.py | 63 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 25 deletions(-) (limited to 'git/repo/base.py') diff --git a/git/repo/base.py b/git/repo/base.py index 35ff68b0..7473c52e 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -25,7 +25,6 @@ from git.exc import ( GitCommandError, InvalidGitRepositoryError, NoSuchPathError, - UnsafeOptionsUsedError, ) from git.index import IndexFile from git.objects import Submodule, RootModule, Commit @@ -133,7 +132,18 @@ class Repo(object): re_envvars = re.compile(r"(\$(\{\s?)?[a-zA-Z_]\w*(\}\s?)?|%\s?[a-zA-Z_]\w*\s?%)") re_author_committer_start = re.compile(r"^(author|committer)") re_tab_full_line = re.compile(r"^\t(.*)$") - re_config_protocol_option = re.compile(r"-[-]?c(|onfig)\s+protocol\.", re.I) + + unsafe_git_clone_options = [ + # This option allows users to execute arbitrary commands. + # https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---upload-packltupload-packgt + "--upload-pack", + "-u", + # Users can override configuration variables + # like `protocol.allow` or `core.gitProxy` to execute arbitrary commands. + # https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---configltkeygtltvaluegt + "--config", + "-c", + ] # invariants # represents the configuration level of a configuration file @@ -961,7 +971,7 @@ class Repo(object): file: str, incremental: bool = False, rev_opts: Optional[List[str]] = None, - **kwargs: Any + **kwargs: Any, ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None: """The blame information for the given file at the given revision. @@ -1152,6 +1162,8 @@ class Repo(object): odb_default_type: Type[GitCmdObjectDB], progress: Union["RemoteProgress", "UpdateProgress", Callable[..., "RemoteProgress"], None] = None, multi_options: Optional[List[str]] = None, + allow_unsafe_protocols: bool = False, + allow_unsafe_options: bool = False, **kwargs: Any, ) -> "Repo": odbt = kwargs.pop("odbt", odb_default_type) @@ -1173,6 +1185,12 @@ class Repo(object): multi = None if multi_options: multi = shlex.split(" ".join(multi_options)) + + if not allow_unsafe_protocols: + Git.check_unsafe_protocols(str(url)) + if not allow_unsafe_options and multi_options: + Git.check_unsafe_options(options=multi_options, unsafe_options=cls.unsafe_git_clone_options) + proc = git.clone( multi, "--", @@ -1221,27 +1239,13 @@ class Repo(object): # END handle remote repo return repo - @classmethod - def unsafe_options( - cls, - url: str, - multi_options: Optional[List[str]] = None, - ) -> bool: - if "ext::" in url: - return True - if multi_options is not None: - if any(["--upload-pack" in m for m in multi_options]): - return True - if any([re.match(cls.re_config_protocol_option, m) for m in multi_options]): - return True - return False - def clone( self, path: PathLike, progress: Optional[Callable] = None, multi_options: Optional[List[str]] = None, - unsafe_protocols: bool = False, + allow_unsafe_protocols: bool = False, + allow_unsafe_options: bool = False, **kwargs: Any, ) -> "Repo": """Create a clone from this repository. @@ -1259,8 +1263,6 @@ class Repo(object): * All remaining keyword arguments are given to the git-clone command :return: ``git.Repo`` (the newly cloned repo)""" - if not unsafe_protocols and self.unsafe_options(path, multi_options): - raise UnsafeOptionsUsedError(f"{path} requires unsafe_protocols flag") return self._clone( self.git, self.common_dir, @@ -1268,6 +1270,8 @@ class Repo(object): type(self.odb), progress, multi_options, + allow_unsafe_protocols=allow_unsafe_protocols, + allow_unsafe_options=allow_unsafe_options, **kwargs, ) @@ -1279,7 +1283,8 @@ class Repo(object): progress: Optional[Callable] = None, env: Optional[Mapping[str, str]] = None, multi_options: Optional[List[str]] = None, - unsafe_protocols: bool = False, + allow_unsafe_protocols: bool = False, + allow_unsafe_options: bool = False, **kwargs: Any, ) -> "Repo": """Create a clone from the given URL @@ -1300,9 +1305,17 @@ class Repo(object): git = cls.GitCommandWrapperType(os.getcwd()) if env is not None: git.update_environment(**env) - if not unsafe_protocols and cls.unsafe_options(url, multi_options): - raise UnsafeOptionsUsedError(f"{url} requires unsafe_protocols flag") - return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs) + return cls._clone( + git, + url, + to_path, + GitCmdObjectDB, + progress, + multi_options, + allow_unsafe_protocols=allow_unsafe_protocols, + allow_unsafe_options=allow_unsafe_options, + **kwargs, + ) def archive( self, -- cgit v1.2.1