Block insecure options and protocols by default by stsewd · Pull Request #1521 · gitpython-developers/GitPython · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
53 changes: 49 additions & 4 deletions git/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
from __future__ import annotations
import re
from contextlib import contextmanager
import io
import logging
Expand All @@ -24,7 +25,7 @@
from git.exc import CommandError
from git.util import is_cygwin_git, cygpath, expand_path, remove_password_if_present

from .exc import GitCommandError, GitCommandNotFound
from .exc import GitCommandError, GitCommandNotFound, UnsafeOptionError, UnsafeProtocolError
from .util import (
LazyMixin,
stream_copy,
Expand Down Expand Up @@ -262,6 +263,8 @@ class Git(LazyMixin):

_excluded_ = ("cat_file_all", "cat_file_header", "_version_info")

re_unsafe_protocol = re.compile("(.+)::.+")

def __getstate__(self) -> Dict[str, Any]:
return slots_to_dict(self, exclude=self._excluded_)

Expand Down Expand Up @@ -454,6 +457,48 @@ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike:
url = url.replace("\\\\", "\\").replace("\\", "/")
return url

@classmethod
def check_unsafe_protocols(cls, url: str) -> None:
"""
Check for unsafe protocols.

Apart from the usual protocols (http, git, ssh),
Git allows "remote helpers" that have the form `<transport>::<address>`,
one of these helpers (`ext::`) can be used to invoke any arbitrary command.

See:

- https://git-scm.com/docs/gitremote-helpers
- https://git-scm.com/docs/git-remote-ext
"""
match = cls.re_unsafe_protocol.match(url)
if match:
protocol = match.group(1)
raise UnsafeProtocolError(
f"The `{protocol}::` protocol looks suspicious, use `allow_unsafe_protocols=True` to allow it."
)

@classmethod
def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None:
"""
Check for unsafe options.

Some options that are passed to `git <command>` can be used to execute
arbitrary commands, this are blocked by default.
"""
# Options can be of the form `foo` or `--foo bar` `--foo=bar`,
# so we need to check if they start with "--foo" or if they are equal to "foo".
bare_unsafe_options = [
option.lstrip("-")
for option in unsafe_options
]
for option in options:
for unsafe_option, bare_option in zip(unsafe_options, bare_unsafe_options):
if option.startswith(unsafe_option) or option == bare_option:
raise UnsafeOptionError(
f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
)

class AutoInterrupt(object):
"""Kill/Interrupt the stored process instance once this instance goes out of scope. It is
used to prevent processes piling up in case iterators stop reading.
Expand Down Expand Up @@ -1148,12 +1193,12 @@ def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any
return args

@classmethod
def __unpack_args(cls, arg_list: Sequence[str]) -> List[str]:
def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]:

outlist = []
if isinstance(arg_list, (list, tuple)):
for arg in arg_list:
outlist.extend(cls.__unpack_args(arg))
outlist.extend(cls._unpack_args(arg))
else:
outlist.append(str(arg_list))

Expand Down Expand Up @@ -1238,7 +1283,7 @@ def _call_process(
# Prepare the argument list

opt_args = self.transform_kwargs(**opts_kwargs)
ext_args = self.__unpack_args([a for a in args if a is not None])
ext_args = self._unpack_args([a for a in args if a is not None])

if insert_after_this_arg is None:
args_list = opt_args + ext_args
Expand Down
8 changes: 8 additions & 0 deletions git/exc.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ class NoSuchPathError(GitError, OSError):
"""Thrown if a path could not be access by the system."""


class UnsafeProtocolError(GitError):
"""Thrown if unsafe protocols are passed without being explicitly allowed."""


class UnsafeOptionError(GitError):
"""Thrown if unsafe options are passed without being explicitly allowed."""


class CommandError(GitError):
"""Base class for exceptions thrown at every stage of `Popen()` execution.

Expand Down
36 changes: 33 additions & 3 deletions git/objects/submodule/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,16 @@ def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> Path
# end

@classmethod
def _clone_repo(cls, repo: "Repo", url: str, path: PathLike, name: str, **kwargs: Any) -> "Repo":
def _clone_repo(
cls,
repo: "Repo",
url: str,
path: PathLike,
name: str,
allow_unsafe_options: bool = False,
allow_unsafe_protocols: bool = False,
**kwargs: Any,
) -> "Repo":
""":return: Repo instance of newly cloned repository
:param repo: our parent repository
:param url: url to clone from
Expand All @@ -289,7 +298,13 @@ def _clone_repo(cls, repo: "Repo", url: str, path: PathLike, name: str, **kwargs
module_checkout_path = osp.join(str(repo.working_tree_dir), path)
# end

clone = git.Repo.clone_from(url, module_checkout_path, **kwargs)
clone = git.Repo.clone_from(
url,
module_checkout_path,
allow_unsafe_options=allow_unsafe_options,
allow_unsafe_protocols=allow_unsafe_protocols,
**kwargs,
)
if cls._need_gitfile_submodules(repo.git):
cls._write_git_file_and_module_config(module_checkout_path, module_abspath)
# end
Expand Down Expand Up @@ -359,6 +374,8 @@ def add(
depth: Union[int, None] = None,
env: Union[Mapping[str, str], None] = None,
clone_multi_options: Union[Sequence[TBD], None] = None,
allow_unsafe_options: bool = False,
allow_unsafe_protocols: bool = False,
) -> "Submodule":
"""Add a new submodule to the given repository. This will alter the index
as well as the .gitmodules file, but will not create a new commit.
Expand Down Expand Up @@ -475,7 +492,16 @@ def add(
kwargs["multi_options"] = clone_multi_options

# _clone_repo(cls, repo, url, path, name, **kwargs):
mrepo = cls._clone_repo(repo, url, path, name, env=env, **kwargs)
mrepo = cls._clone_repo(
repo,
url,
path,
name,
env=env,
allow_unsafe_options=allow_unsafe_options,
allow_unsafe_protocols=allow_unsafe_protocols,
**kwargs,
)
# END verify url

## See #525 for ensuring git urls in config-files valid under Windows.
Expand Down Expand Up @@ -520,6 +546,8 @@ def update(
keep_going: bool = False,
env: Union[Mapping[str, str], None] = None,
clone_multi_options: Union[Sequence[TBD], None] = None,
allow_unsafe_options: bool = False,
allow_unsafe_protocols: bool = False,
) -> "Submodule":
"""Update the repository of this submodule to point to the checkout
we point at with the binsha of this instance.
Expand Down Expand Up @@ -643,6 +671,8 @@ def update(
n=True,
env=env,
multi_options=clone_multi_options,
allow_unsafe_options=allow_unsafe_options,
allow_unsafe_protocols=allow_unsafe_protocols,
)
# END handle dry-run
progress.update(
Expand Down
70 changes: 63 additions & 7 deletions git/remote.py
Loading