Skip to content

Commit a60991f

Browse files
committed
safe mode to disable executing any external programs except git
1 parent 85c8155 commit a60991f

File tree

4 files changed

+273
-13
lines changed

4 files changed

+273
-13
lines changed

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
)
@@ -627,6 +628,7 @@ class Git(metaclass=_GitMeta):
627628

628629
__slots__ = (
629630
"_working_dir",
631+
"_safe",
630632
"cat_file_all",
631633
"cat_file_header",
632634
"_version_info",
@@ -961,17 +963,56 @@ def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) ->
961963

962964
CatFileContentStream: TypeAlias = _CatFileContentStream
963965

964-
def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
966+
def __init__(self, working_dir: Union[None, PathLike] = None, safe: bool = False) -> None:
965967
"""Initialize this instance with:
966968
967969
:param working_dir:
968970
Git directory we should work in. If ``None``, we always work in the current
969971
directory as returned by :func:`os.getcwd`.
970972
This is meant to be the working tree directory if available, or the
971973
``.git`` directory in case of bare repositories.
974+
975+
:param safe:
976+
Lock down the configuration to make it as safe as possible
977+
when working with publicly accessible, untrusted
978+
repositories. This disables all known options that can run
979+
external programs and limits networking to the HTTP protocol
980+
via ``https://`` URLs. This might not cover Git config
981+
options that were added since this was implemented, or
982+
options that have unknown exploit vectors. It is a best
983+
effort defense rather than an exhaustive protection measure.
984+
985+
In order to make this more likely to work with submodules,
986+
some attempts are made to rewrite remote URLs to ``https://``
987+
using `insteadOf` in the config. This might not work on all
988+
projects, so submodules should always use ``https://`` URLs.
989+
990+
:envvar:`GIT_TERMINAL_PROMPT` is set to `false` and these
991+
environment variables are forced to `/bin/true`:
992+
:envvar:`GIT_ASKPASS`, :envvar:`GIT_EDITOR`,
993+
:envvar:`GIT_PAGER`, :envvar:`GIT_SSH`,
994+
:envvar:`GIT_SSH_COMMAND`, and :envvar:`SSH_ASKPASS`.
995+
996+
Git config options are supplied via the command line to set
997+
up key parts of safe mode.
998+
999+
- Direct options for executing external commands are set to ``/bin/true``:
1000+
``core.askpass``, ``core.sshCommand`` and ``credential.helper``.
1001+
1002+
- External password prompts are disabled by skipping authentication using
1003+
``http.emptyAuth=true``.
1004+
1005+
- Any use of an fsmonitor daemon is disabled using ``core.fsmonitor=false``.
1006+
1007+
- Hook scripts are disabled using ``core.hooksPath=/dev/null``.
1008+
1009+
It was not possible to cover all config items that might execute an external
1010+
command, for example, ``receive.procReceiveRefs``,
1011+
``uploadpack.packObjectsHook`` and ``remote.<name>.vcs``.
9721012
"""
9731013
super().__init__()
9741014
self._working_dir = expand_path(working_dir)
1015+
self._safe = safe
9751016
self._git_options: Union[List[str], Tuple[str, ...]] = ()
9761017
self._persistent_git_options: List[str] = []
9771018

@@ -1218,6 +1259,8 @@ def execute(
12181259
12191260
:raise git.exc.GitCommandError:
12201261
1262+
:raise git.exc.UnsafeExecutionError:
1263+
12211264
:note:
12221265
If you add additional keyword arguments to the signature of this method, you
12231266
must update the ``execute_kwargs`` variable housed in this module.
@@ -1227,6 +1270,64 @@ def execute(
12271270
if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process):
12281271
_logger.info(" ".join(redacted_command))
12291272

1273+
if shell is None:
1274+
# Get the value of USE_SHELL with no deprecation warning. Do this without
1275+
# warnings.catch_warnings, to avoid a race condition with application code
1276+
# configuring warnings. The value could be looked up in type(self).__dict__
1277+
# or Git.__dict__, but those can break under some circumstances. This works
1278+
# the same as self.USE_SHELL in more situations; see Git.__getattribute__.
1279+
shell = super().__getattribute__("USE_SHELL")
1280+
1281+
if self._safe:
1282+
if shell:
1283+
raise UnsafeExecutionError(
1284+
redacted_command,
1285+
"Command cannot be executed in a shell when in safe mode.",
1286+
)
1287+
if not isinstance(command, Sequence):
1288+
raise UnsafeExecutionError(
1289+
redacted_command,
1290+
"Command must be a Sequence to be executed in safe mode.",
1291+
)
1292+
if command[0] != self.GIT_PYTHON_GIT_EXECUTABLE:
1293+
raise UnsafeExecutionError(
1294+
redacted_command,
1295+
f'Only "{self.GIT_PYTHON_GIT_EXECUTABLE}" can be executed when in safe mode.',
1296+
)
1297+
config_args = [
1298+
"-c",
1299+
"core.askpass=/bin/true",
1300+
"-c",
1301+
"core.fsmonitor=false",
1302+
"-c",
1303+
"core.hooksPath=/dev/null",
1304+
"-c",
1305+
"core.sshCommand=/bin/true",
1306+
"-c",
1307+
"credential.helper=/bin/true",
1308+
"-c",
1309+
"http.emptyAuth=true",
1310+
"-c",
1311+
"protocol.allow=never",
1312+
"-c",
1313+
"protocol.https.allow=always",
1314+
"-c",
1315+
"url.https://bitbucket.org/.insteadOf=git@bitbucket.org:",
1316+
"-c",
1317+
"url.https://codeberg.org/.insteadOf=git@codeberg.org:",
1318+
"-c",
1319+
"url.https://github.com/.insteadOf=git@github.com:",
1320+
"-c",
1321+
"url.https://gitlab.com/.insteadOf=git@gitlab.com:",
1322+
"-c",
1323+
"url.https://.insteadOf=git://",
1324+
"-c",
1325+
"url.https://.insteadOf=http://",
1326+
"-c",
1327+
"url.https://.insteadOf=ssh://",
1328+
]
1329+
command = [command.pop(0)] + config_args + command
1330+
12301331
# Allow the user to have the command executed in their working dir.
12311332
try:
12321333
cwd = self._working_dir or os.getcwd() # type: Union[None, str]
@@ -1244,6 +1345,15 @@ def execute(
12441345
# just to be sure.
12451346
env["LANGUAGE"] = "C"
12461347
env["LC_ALL"] = "C"
1348+
# Globally disable things that can execute commands, including password prompts.
1349+
if self._safe:
1350+
env["GIT_ASKPASS"] = "/bin/true"
1351+
env["GIT_EDITOR"] = "/bin/true"
1352+
env["GIT_PAGER"] = "/bin/true"
1353+
env["GIT_SSH"] = "/bin/true"
1354+
env["GIT_SSH_COMMAND"] = "/bin/true"
1355+
env["GIT_TERMINAL_PROMPT"] = "false"
1356+
env["SSH_ASKPASS"] = "/bin/true"
12471357
env.update(self._environment)
12481358
if inline_env is not None:
12491359
env.update(inline_env)
@@ -1260,13 +1370,6 @@ def execute(
12601370
# END handle
12611371

12621372
stdout_sink = PIPE if with_stdout else getattr(subprocess, "DEVNULL", None) or open(os.devnull, "wb")
1263-
if shell is None:
1264-
# Get the value of USE_SHELL with no deprecation warning. Do this without
1265-
# warnings.catch_warnings, to avoid a race condition with application code
1266-
# configuring warnings. The value could be looked up in type(self).__dict__
1267-
# or Git.__dict__, but those can break under some circumstances. This works
1268-
# the same as self.USE_SHELL in more situations; see Git.__getattribute__.
1269-
shell = super().__getattribute__("USE_SHELL")
12701373
_logger.debug(
12711374
"Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)",
12721375
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy