← 返回首页
safe mode to disable executing any external programs except git · gitpython-developers/GitPython@56e3440 · GitHub
Skip to content

Navigation Menu

Toggle navigation
Sign in
Appearance settings
Search or jump to...

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

Provide feedback

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

Include my email address so I can be contacted

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Resetting focus

Commit 56e3440

Browse files
committed
safe mode to disable executing any external programs except git
1 parent 5a294a6 commit 56e3440

4 files changed

Lines changed: 273 additions & 14 deletions

File tree

‎git/cmd.py‎

Lines changed: 111 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
CommandError,
2727
GitCommandError,
2828
GitCommandNotFound,
29+
UnsafeExecutionError,
2930
UnsafeOptionError,
3031
UnsafeProtocolError,
3132
)
@@ -631,6 +632,7 @@ class Git(metaclass=_GitMeta):
631632

632633
__slots__ = (
633634
"_working_dir",
635+
"_safe",
634636
"cat_file_all",
635637
"cat_file_header",
636638
"_version_info",
@@ -977,17 +979,56 @@ def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) ->
977979

978980
CatFileContentStream: TypeAlias = _CatFileContentStream
979981

980-
def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
982+
def __init__(self, working_dir: Union[None, PathLike] = None, safe: bool = False) -> None:
981983
"""Initialize this instance with:
982984
983985
:param working_dir:
984986
Git directory we should work in. If ``None``, we always work in the current
985987
directory as returned by :func:`os.getcwd`.
986988
This is meant to be the working tree directory if available, or the
987989
``.git`` directory in case of bare repositories.
990+
991+
:param safe:
992+
Lock down the configuration to make it as safe as possible
993+
when working with publicly accessible, untrusted
994+
repositories. This disables all known options that can run
995+
external programs and limits networking to the HTTP protocol
996+
via ``https://`` URLs. This might not cover Git config
997+
options that were added since this was implemented, or
998+
options that have unknown exploit vectors. It is a best
999+
effort defense rather than an exhaustive protection measure.
1000+
1001+
In order to make this more likely to work with submodules,
1002+
some attempts are made to rewrite remote URLs to ``https://``
1003+
using `insteadOf` in the config. This might not work on all
1004+
projects, so submodules should always use ``https://`` URLs.
1005+
1006+
:envvar:`GIT_TERMINAL_PROMPT` is set to `false` and these
1007+
environment variables are forced to `/bin/true`:
1008+
:envvar:`GIT_ASKPASS`, :envvar:`GIT_EDITOR`,
1009+
:envvar:`GIT_PAGER`, :envvar:`GIT_SSH`,
1010+
:envvar:`GIT_SSH_COMMAND`, and :envvar:`SSH_ASKPASS`.
1011+
1012+
Git config options are supplied via the command line to set
1013+
up key parts of safe mode.
1014+
1015+
- Direct options for executing external commands are set to ``/bin/true``:
1016+
``core.askpass``, ``core.sshCommand`` and ``credential.helper``.
1017+
1018+
- External password prompts are disabled by skipping authentication using
1019+
``http.emptyAuth=true``.
1020+
1021+
- Any use of an fsmonitor daemon is disabled using ``core.fsmonitor=false``.
1022+
1023+
- Hook scripts are disabled using ``core.hooksPath=/dev/null``.
1024+
1025+
It was not possible to cover all config items that might execute an external
1026+
command, for example, ``receive.procReceiveRefs``,
1027+
``uploadpack.packObjectsHook`` and ``remote.<name>.vcs``.
9881028
"""
9891029
super().__init__()
9901030
self._working_dir = expand_path(working_dir)
1031+
self._safe = safe
9911032
self._git_options: Union[List[str], Tuple[str, ...]] = ()
9921033
self._persistent_git_options: List[str] = []
9931034

@@ -1234,6 +1275,8 @@ def execute(
12341275
12351276
:raise git.exc.GitCommandError:
12361277
1278+
:raise git.exc.UnsafeExecutionError:
1279+
12371280
:note:
12381281
If you add additional keyword arguments to the signature of this method, you
12391282
must update the ``execute_kwargs`` variable housed in this module.
@@ -1243,6 +1286,64 @@ def execute(
12431286
if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process):
12441287
_logger.info(" ".join(redacted_command))
12451288

1289+
if shell is None:
1290+
# Get the value of USE_SHELL with no deprecation warning. Do this without
1291+
# warnings.catch_warnings, to avoid a race condition with application code
1292+
# configuring warnings. The value could be looked up in type(self).__dict__
1293+
# or Git.__dict__, but those can break under some circumstances. This works
1294+
# the same as self.USE_SHELL in more situations; see Git.__getattribute__.
1295+
shell = super().__getattribute__("USE_SHELL")
1296+
1297+
if self._safe:
1298+
if shell:
1299+
raise UnsafeExecutionError(
1300+
redacted_command,
1301+
"Command cannot be executed in a shell when in safe mode.",
1302+
)
1303+
if not isinstance(command, Sequence):
1304+
raise UnsafeExecutionError(
1305+
redacted_command,
1306+
"Command must be a Sequence to be executed in safe mode.",
1307+
)
1308+
if command[0] != self.GIT_PYTHON_GIT_EXECUTABLE:
1309+
raise UnsafeExecutionError(
1310+
redacted_command,
1311+
f'Only "{self.GIT_PYTHON_GIT_EXECUTABLE}" can be executed when in safe mode.',
1312+
)
1313+
config_args = [
1314+
"-c",
1315+
"core.askpass=/bin/true",
1316+
"-c",
1317+
"core.fsmonitor=false",
1318+
"-c",
1319+
"core.hooksPath=/dev/null",
1320+
"-c",
1321+
"core.sshCommand=/bin/true",
1322+
"-c",
1323+
"credential.helper=/bin/true",
1324+
"-c",
1325+
"http.emptyAuth=true",
1326+
"-c",
1327+
"protocol.allow=never",
1328+
"-c",
1329+
"protocol.https.allow=always",
1330+
"-c",
1331+
"url.https://bitbucket.org/.insteadOf=git@bitbucket.org:",
1332+
"-c",
1333+
"url.https://codeberg.org/.insteadOf=git@codeberg.org:",
1334+
"-c",
1335+
"url.https://github.com/.insteadOf=git@github.com:",
1336+
"-c",
1337+
"url.https://gitlab.com/.insteadOf=git@gitlab.com:",
1338+
"-c",
1339+
"url.https://.insteadOf=git://",
1340+
"-c",
1341+
"url.https://.insteadOf=http://",
1342+
"-c",
1343+
"url.https://.insteadOf=ssh://",
1344+
]
1345+
command = [command.pop(0)] + config_args + command
1346+
12461347
# Allow the user to have the command executed in their working dir.
12471348
try:
12481349
cwd = self._working_dir or os.getcwd() # type: Union[None, str]
@@ -1260,6 +1361,15 @@ def execute(
12601361
# just to be sure.
12611362
env["LANGUAGE"] = "C"
12621363
env["LC_ALL"] = "C"
1364+
# Globally disable things that can execute commands, including password prompts.
1365+
if self._safe:
1366+
env["GIT_ASKPASS"] = "/bin/true"
1367+
env["GIT_EDITOR"] = "/bin/true"
1368+
env["GIT_PAGER"] = "/bin/true"
1369+
env["GIT_SSH"] = "/bin/true"
1370+
env["GIT_SSH_COMMAND"] = "/bin/true"
1371+
env["GIT_TERMINAL_PROMPT"] = "false"
1372+
env["SSH_ASKPASS"] = "/bin/true"
12631373
env.update(self._environment)
12641374
if inline_env is not None:
12651375
env.update(inline_env)
@@ -1276,13 +1386,6 @@ def execute(
12761386
# END handle
12771387

12781388
stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
1279-
if shell is None:
1280-
# Get the value of USE_SHELL with no deprecation warning. Do this without
1281-
# warnings.catch_warnings, to avoid a race condition with application code
1282-
# configuring warnings. The value could be looked up in type(self).__dict__
1283-
# or Git.__dict__, but those can break under some circumstances. This works
1284-
# the same as self.USE_SHELL in more situations; see Git.__getattribute__.
1285-
shell = super().__getattribute__("USE_SHELL")
12861389
_logger.debug(
12871390
"Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)",
12881391
redacted_command,

‎git/exc.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ def __init__(
159159
super().__init__(command, status, stderr, stdout)
160160

161161

162+
class UnsafeExecutionError(CommandError):
163+
"""Thrown if anything but git is executed when in safe mode."""
164+
165+
162166
class CheckoutError(GitError):
163167
"""Thrown if a file could not be checked out from the index as it contained
164168
changes.

0 commit comments

Comments
 (0)

Footer

© 2026 GitHub, Inc.