From 509d0c5210e19cc88c61476bb5d337de4b6152c8 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 26 May 2025 16:10:14 +0200 Subject: [PATCH] safe mode to disable executing any external programs except git --- git/cmd.py | 27 ++++++++++++++++++++++++++- git/repo/base.py | 29 ++++++++++++++++++++++++----- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 2048a43fa..af789c405 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -398,6 +398,7 @@ class Git(metaclass=_GitMeta): __slots__ = ( "_working_dir", + "_safe", "cat_file_all", "cat_file_header", "_version_info", @@ -944,7 +945,7 @@ def __del__(self) -> None: self._stream.read(bytes_left + 1) # END handle incomplete read - def __init__(self, working_dir: Union[None, PathLike] = None) -> None: + def __init__(self, working_dir: Union[None, PathLike] = None, safe: bool = False) -> None: """Initialize this instance with: :param working_dir: @@ -952,9 +953,12 @@ def __init__(self, working_dir: Union[None, PathLike] = None) -> None: directory as returned by :func:`os.getcwd`. This is meant to be the working tree directory if available, or the ``.git`` directory in case of bare repositories. + + TODO :param safe: """ super().__init__() self._working_dir = expand_path(working_dir) + self._safe = safe self._git_options: Union[List[str], Tuple[str, ...]] = () self._persistent_git_options: List[str] = [] @@ -1205,6 +1209,21 @@ def execute( If you add additional keyword arguments to the signature of this method, you must update the ``execute_kwargs`` variable housed in this module. """ + if self._safe: + if isinstance(command, str): + command = [command] + config_args = [ + '-c', 'core.askpass=/bin/true', + '-c', 'core.hooksPath=/dev/null', + '-c', 'core.sshCommand=/bin/true', + '-c', 'credential.helper=/bin/true', + '-c', 'http.emptyAuth=true', + '-c', 'protocol.allow=never', + '-c', 'protocol.https.allow=always', + '-c', 'url.https://.insteadOf=ssh://', + ] + command = [command.pop(0)] + config_args + command + # Remove password for the command if present. redacted_command = remove_password_if_present(command) if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process): @@ -1227,6 +1246,12 @@ def execute( # just to be sure. env["LANGUAGE"] = "C" env["LC_ALL"] = "C" + # Globally disable things that can execute commands, including password prompts. + if self._safe: + env['GIT_TERMINAL_PROMPT'] = 'false' + env['GIT_ASKPASS'] = '/bin/true' + env['SSH_ASKPASS'] = '/bin/true' + env['GIT_SSH'] = '/bin/true' env.update(self._environment) if inline_env is not None: env.update(inline_env) diff --git a/git/repo/base.py b/git/repo/base.py index 7e918df8c..0d8da8d76 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -131,6 +131,9 @@ class Repo: git_dir: PathLike """The ``.git`` repository directory.""" + safe: None + """Whether this is operating using restricted protocol and execution access.""" + _common_dir: PathLike = "" # Precompiled regex @@ -175,6 +178,7 @@ def __init__( odbt: Type[LooseObjectDB] = GitCmdObjectDB, search_parent_directories: bool = False, expand_vars: bool = True, + safe: bool = False, ) -> None: R"""Create a new :class:`Repo` instance. @@ -204,6 +208,11 @@ def __init__( Please note that this was the default behaviour in older versions of GitPython, which is considered a bug though. + :param safe: + Lock down the configuration to make it as safe as possible + when working with publicly accessible, untrusted + repositories. + :raise git.exc.InvalidGitRepositoryError: :raise git.exc.NoSuchPathError: @@ -235,6 +244,8 @@ def __init__( if not os.path.exists(epath): raise NoSuchPathError(epath) + self.safe = safe + # Walk up the path to find the `.git` dir. curpath = epath git_dir = None @@ -289,6 +300,8 @@ def __init__( raise InvalidGitRepositoryError(epath) self.git_dir = git_dir + self.safe = safe + self._bare = False try: self._bare = self.config_reader("repository").getboolean("core", "bare") @@ -309,7 +322,7 @@ def __init__( # END working dir handling self.working_dir: PathLike = self._working_tree_dir or self.common_dir - self.git = self.GitCommandWrapperType(self.working_dir) + self.git = self.GitCommandWrapperType(self.working_dir, safe) # Special handling, in special times. rootpath = osp.join(self.common_dir, "objects") @@ -1305,6 +1318,7 @@ def init( mkdir: bool = True, odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, expand_vars: bool = True, + safe: bool = False, **kwargs: Any, ) -> "Repo": """Initialize a git repository at the given path if specified. @@ -1329,6 +1343,8 @@ def init( information disclosure, allowing attackers to access the contents of environment variables. + TODO :param safe: + :param kwargs: Keyword arguments serving as additional options to the :manpage:`git-init(1)` command. @@ -1342,9 +1358,9 @@ def init( os.makedirs(path, 0o755) # git command automatically chdir into the directory - git = cls.GitCommandWrapperType(path) + git = cls.GitCommandWrapperType(path, safe) git.init(**kwargs) - return cls(path, odbt=odbt) + return cls(path, odbt=odbt, safe=safe) @classmethod def _clone( @@ -1357,6 +1373,7 @@ def _clone( multi_options: Optional[List[str]] = None, allow_unsafe_protocols: bool = False, allow_unsafe_options: bool = False, + safe: bool = False, **kwargs: Any, ) -> "Repo": odbt = kwargs.pop("odbt", odb_default_type) @@ -1418,7 +1435,7 @@ def _clone( if not osp.isabs(path): path = osp.join(git._working_dir, path) if git._working_dir is not None else path - repo = cls(path, odbt=odbt) + repo = cls(path, odbt=odbt, safe=safe) # Retain env values that were passed to _clone(). repo.git.update_environment(**git.environment()) @@ -1501,6 +1518,7 @@ def clone_from( multi_options: Optional[List[str]] = None, allow_unsafe_protocols: bool = False, allow_unsafe_options: bool = False, + safe: bool = False, **kwargs: Any, ) -> "Repo": """Create a clone from the given URL. @@ -1537,7 +1555,7 @@ def clone_from( :return: :class:`Repo` instance pointing to the cloned directory. """ - git = cls.GitCommandWrapperType(os.getcwd()) + git = cls.GitCommandWrapperType(os.getcwd(), safe) if env is not None: git.update_environment(**env) return cls._clone( @@ -1549,6 +1567,7 @@ def clone_from( multi_options, allow_unsafe_protocols=allow_unsafe_protocols, allow_unsafe_options=allow_unsafe_options, + safe=safe, **kwargs, )