Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Draft: safe mode to disable executing any external programs except git#2029

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
eighthave wants to merge1 commit intogitpython-developers:main
base:main
Choose a base branch
Loading
fromeighthave:safe-mode
Draft
Show file tree
Hide file tree
Changes fromall 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
63 changes: 62 additions & 1 deletiongit/cmd.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,7 @@
CommandError,
GitCommandError,
GitCommandNotFound,
UnsafeExecutionError,
UnsafeOptionError,
UnsafeProtocolError,
)
Expand DownExpand Up@@ -398,6 +399,7 @@ class Git(metaclass=_GitMeta):

__slots__ = (
"_working_dir",
"_safe",
"cat_file_all",
"cat_file_header",
"_version_info",
Expand DownExpand Up@@ -944,17 +946,20 @@ 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:
Git directory we should work in. If ``None``, we always work in the current
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] = []

Expand DownExpand Up@@ -1201,6 +1206,8 @@ def execute(

:raise git.exc.GitCommandError:

:raise git.exc.UnsafeExecutionError:

:note:
If you add additional keyword arguments to the signature of this method, you
must update the ``execute_kwargs`` variable housed in this module.
Expand All@@ -1210,6 +1217,51 @@ def execute(
if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process):
_logger.info(" ".join(redacted_command))

if self._safe:
if isinstance(command, str) or command[0] != self.GIT_PYTHON_GIT_EXECUTABLE:
raise UnsafeExecutionError(
redacted_command,
f'Only {self.GIT_PYTHON_GIT_EXECUTABLE} can be executed when in safe mode.',
)
if shell:
raise UnsafeExecutionError(
redacted_command,
f'Command cannot be executed in a shell when in safe mode.',
)
config_args = [
Copy link
Member

@EliahKaganEliahKaganMay 28, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Theseconfig_args passing command-scope Git configuration variables via-c are only meaningful if the executable being run isgit or otherwise recognizesgit options. But thisGit.execute method is not limited to runninggit. It is intentionally usable to run any command in an environment that theGit object would rungit in, in a manner similar to how it would rungit.

Even if I am right to think, as expressed in#2029 (comment), that "safe mode" need not inherently preclude other subprocess invocations from occurring so long as they are not happening throughgit, it seems to me that users may intuitively assume that the protections apply to all use ofexecute when "safe mode" is enabled and not overridden in any way. It also seems like that is part of the intent here.

Furthermore, sinceexecute uses state associated with the repository--itscwd in particular--I think the risks "safe mode" is meant to mitigate can only be mitigated if its protections apply to allexecute calls for which "safe mode" is considered to be in effect. And ifexecute is used to run something that itself runsgit, such as a script ofgit commands, then it could be a problem for the necessary command-scope configuration not to be in effect.

It seems to me that a simple and effective solution to the problem thatgit options could be passed to programs other thangit is therefore for "safe mode" to enforce, not merely thatcommand is the right kind of object (see above), but also that its first element is exactly what it would be whenexecute is called through_call_process--that is, that the first element is equal toGit.GIT_PYTHON_GIT_EXECUTABLE.


If for some reason it is necessary in "safe mode" forexecute to run other programs besides thegit executable that_call_process would use, then it might be possible to pass the command-scope configuration entirely using environment variables, either always or in the case that the command is notGit.GIT_PYTHON_GIT_EXECUTABLE.

However, I think this would be challenging, both in general because of the increased complexity it would entail, and more specifically because, of the two ways to pass command-scope Git configuration variables using environment variables:

  • GIT_CONFIG_{COUNT,KEY,VALUE} is not recognized by all versions ofgit in current use (some distributions maintain old versions with downstream security patches but not new features).
  • GIT_CONFIG_PARAMETERS is considered an implementation detail ofgit with two different syntaxes so fardepending on thegit version.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

It seems to me that a simple and effective solution to the problem that git options could be passed to programs other than git is therefore for "safe mode" to enforce, not merely that command is the right kind of object (see above), but also that its first element is exactly what it would be when execute is called through _call_process--that is, that the first element is equal to Git.GIT_PYTHON_GIT_EXECUTABLE.

Thanks for this detailed analysis! I agree with this approach. I'll try implementing it. I was not aware that GitPython is executing other things besidesgit. Makes sense to me that safe mode should also restrict those as well.

"-c",
"core.askpass=/bin/true",
"-c",
"core.fsmonitor=false",
"-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://bitbucket.org/.insteadOf=git@bitbucket.org:",
"-c",
"url.https://codeberg.org/.insteadOf=git@codeberg.org:",
"-c",
"url.https://github.com/.insteadOf=git@github.com:",
"-c",
"url.https://gitlab.com/.insteadOf=git@gitlab.com:",
"-c",
"url.https://.insteadOf=git://",
"-c",
"url.https://.insteadOf=http://",
"-c",
"url.https://.insteadOf=ssh://",
]
command = [command.pop(0)] + config_args + command

# Allow the user to have the command executed in their working dir.
try:
cwd = self._working_dir or os.getcwd() # type: Union[None, str]
Expand All@@ -1227,6 +1279,15 @@ 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_ASKPASS"] = "/bin/true"
env["GIT_EDITOR"] = "/bin/true"
env["GIT_PAGER"] = "/bin/true"
env["GIT_SSH"] = "/bin/true"
env["GIT_SSH_COMMAND"] = "/bin/true"
env["GIT_TERMINAL_PROMPT"] = "false"
env["SSH_ASKPASS"] = "/bin/true"
env.update(self._environment)
if inline_env is not None:
env.update(inline_env)
Expand Down
4 changes: 4 additions & 0 deletionsgit/exc.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -159,6 +159,10 @@ def __init__(
super().__init__(command, status, stderr, stdout)


class UnsafeExecutionError(CommandError):
"""Thrown if anything but git is executed when in safe mode."""


class CheckoutError(GitError):
"""Thrown if a file could not be checked out from the index as it contained
changes.
Expand Down
40 changes: 35 additions & 5 deletionsgit/repo/base.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand DownExpand Up@@ -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.

Expand DownExpand Up@@ -204,6 +208,17 @@ def __init__(
Please note that this was the default behaviour in older versions of
GitPython, which is considered a bug though.

:param safe:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I think without exhaustive tests and actually challenging the code we wouldn't want to make the flag seem like a perfect defence.
Somehow I feel thatas safe as possible should rather be a disclaimer about this being a best-effort basis and that the defence is probably not complete, while stating what it focusses on.

eighthave reacted with thumbs up emoji
Copy link
Author

@eighthaveeighthaveMay 27, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Agreed. This is what I have so far:

Lock down the configuration to make it as safe as possible when working with publicly accessible, untrusted repositories. This disables all known options that can run an external program and limits networking to the HTTP protocol via https:// URLs. This might not cover Git config options that were added since this was implemented, or options that might have unknown exploit vectors. It is a best effort defense rather than an exhaustive protection measure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

That sounds good! Maybe it makes sense to explicitly mention configuration keys with subsections, as these are particularly hard to pickup.

On the bright side, thinking about it, this would only be a problem if someone else controls the environment or the configuration. Maybe the latter is common for repositories on shared drives?

Lock down the configuration to make it as safe as possible
when working with publicly accessible, untrusted
repositories. This disables all known options that can run
an external program and limits networking to the HTTP
protocol via https:// URLs. This might not cover Git config
options that were added since this was implemented, or
options that might have unknown exploit vectors. It is a
best effort defense rather than an exhaustive protection
measure.

:raise git.exc.InvalidGitRepositoryError:

:raise git.exc.NoSuchPathError:
Expand DownExpand Up@@ -235,6 +250,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
Expand DownExpand Up@@ -309,7 +326,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")
Expand DownExpand Up@@ -1305,6 +1322,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.
Expand All@@ -1329,6 +1347,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.
Expand All@@ -1342,9 +1362,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(
Expand All@@ -1357,6 +1377,7 @@ def _clone(
multi_options: Optional[List[str]] = None,
allow_unsafe_protocols: bool = False,
allow_unsafe_options: bool = False,
safe: Union[bool, None] = None,
**kwargs: Any,
) -> "Repo":
odbt = kwargs.pop("odbt", odb_default_type)
Expand DownExpand Up@@ -1418,7 +1439,11 @@ 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)
# if safe is not explicitly defined, then the new Repo instance should inherit the safe value
if safe is None:
safe = git._safe

repo = cls(path, odbt=odbt, safe=safe)

# Retain env values that were passed to _clone().
repo.git.update_environment(**git.environment())
Expand DownExpand Up@@ -1501,6 +1526,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.
Expand DownExpand Up@@ -1531,13 +1557,16 @@ def clone_from(
:param allow_unsafe_options:
Allow unsafe options to be used, like ``--upload-pack``.

:param safe:
TODO

:param kwargs:
See the :meth:`clone` method.

: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(
Expand All@@ -1549,6 +1578,7 @@ def clone_from(
multi_options,
allow_unsafe_protocols=allow_unsafe_protocols,
allow_unsafe_options=allow_unsafe_options,
safe=safe,
**kwargs,
)

Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp