Skip to content

Commit 2ab2ea9

Browse files
committed
safe mode to disable executing any external programs except git
1 parent bf51609 commit 2ab2ea9

File tree

2 files changed

+86
-6
lines changed

2 files changed

+86
-6
lines changed

git/cmd.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ class Git(metaclass=_GitMeta):
398398

399399
__slots__ = (
400400
"_working_dir",
401+
"_safe",
401402
"cat_file_all",
402403
"cat_file_header",
403404
"_version_info",
@@ -944,17 +945,20 @@ def __del__(self) -> None:
944945
self._stream.read(bytes_left + 1)
945946
# END handle incomplete read
946947

947-
def __init__(self, working_dir: Union[None, PathLike] = None) -> None:
948+
def __init__(self, working_dir: Union[None, PathLike] = None, safe: bool = False) -> None:
948949
"""Initialize this instance with:
949950
950951
:param working_dir:
951952
Git directory we should work in. If ``None``, we always work in the current
952953
directory as returned by :func:`os.getcwd`.
953954
This is meant to be the working tree directory if available, or the
954955
``.git`` directory in case of bare repositories.
956+
957+
TODO :param safe:
955958
"""
956959
super().__init__()
957960
self._working_dir = expand_path(working_dir)
961+
self._safe = safe
958962
self._git_options: Union[List[str], Tuple[str, ...]] = ()
959963
self._persistent_git_options: List[str] = []
960964

@@ -1205,6 +1209,43 @@ def execute(
12051209
If you add additional keyword arguments to the signature of this method, you
12061210
must update the ``execute_kwargs`` variable housed in this module.
12071211
"""
1212+
if self._safe:
1213+
if isinstance(command, str):
1214+
command = [command]
1215+
config_args = [
1216+
"-c",
1217+
"core.askpass=/bin/true",
1218+
"-c",
1219+
"core.fsmonitor=false",
1220+
"-c",
1221+
"core.hooksPath=/dev/null",
1222+
"-c",
1223+
"core.sshCommand=/bin/true",
1224+
"-c",
1225+
"credential.helper=/bin/true",
1226+
"-c",
1227+
"http.emptyAuth=true",
1228+
"-c",
1229+
"protocol.allow=never",
1230+
"-c",
1231+
"protocol.https.allow=always",
1232+
"-c",
1233+
"url.https://bitbucket.org/[email protected]:",
1234+
"-c",
1235+
"url.https://codeberg.org/[email protected]:",
1236+
"-c",
1237+
"url.https://github.com/[email protected]:",
1238+
"-c",
1239+
"url.https://gitlab.com/[email protected]:",
1240+
"-c",
1241+
"url.https://.insteadOf=git://",
1242+
"-c",
1243+
"url.https://.insteadOf=http://",
1244+
"-c",
1245+
"url.https://.insteadOf=ssh://",
1246+
]
1247+
command = [command.pop(0)] + config_args + command
1248+
12081249
# Remove password for the command if present.
12091250
redacted_command = remove_password_if_present(command)
12101251
if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process):
@@ -1227,6 +1268,15 @@ def execute(
12271268
# just to be sure.
12281269
env["LANGUAGE"] = "C"
12291270
env["LC_ALL"] = "C"
1271+
# Globally disable things that can execute commands, including password prompts.
1272+
if self._safe:
1273+
env["GIT_ASKPASS"] = "/bin/true"
1274+
env["GIT_EDITOR"] = "/bin/true"
1275+
env["GIT_PAGER"] = "/bin/true"
1276+
env["GIT_SSH"] = "/bin/true"
1277+
env["GIT_SSH_COMMAND"] = "/bin/true"
1278+
env["GIT_TERMINAL_PROMPT"] = "false"
1279+
env["SSH_ASKPASS"] = "/bin/true"
12301280
env.update(self._environment)
12311281
if inline_env is not None:
12321282
env.update(inline_env)

git/repo/base.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ class Repo:
131131
git_dir: PathLike
132132
"""The ``.git`` repository directory."""
133133

134+
safe: None
135+
"""Whether this is operating using restricted protocol and execution access."""
136+
134137
_common_dir: PathLike = ""
135138

136139
# Precompiled regex
@@ -175,6 +178,7 @@ def __init__(
175178
odbt: Type[LooseObjectDB] = GitCmdObjectDB,
176179
search_parent_directories: bool = False,
177180
expand_vars: bool = True,
181+
safe: bool = False,
178182
) -> None:
179183
R"""Create a new :class:`Repo` instance.
180184
@@ -204,6 +208,17 @@ def __init__(
204208
Please note that this was the default behaviour in older versions of
205209
GitPython, which is considered a bug though.
206210
211+
:param safe:
212+
Lock down the configuration to make it as safe as possible
213+
when working with publicly accessible, untrusted
214+
repositories. This disables all known options that can run
215+
an external program and limits networking to the HTTP
216+
protocol via https:// URLs. This might not cover Git config
217+
options that were added since this was implemented, or
218+
options that might have unknown exploit vectors. It is a
219+
best effort defense rather than an exhaustive protection
220+
measure.
221+
207222
:raise git.exc.InvalidGitRepositoryError:
208223
209224
:raise git.exc.NoSuchPathError:
@@ -235,6 +250,8 @@ def __init__(
235250
if not os.path.exists(epath):
236251
raise NoSuchPathError(epath)
237252

253+
self.safe = safe
254+
238255
# Walk up the path to find the `.git` dir.
239256
curpath = epath
240257
git_dir = None
@@ -309,7 +326,7 @@ def __init__(
309326
# END working dir handling
310327

311328
self.working_dir: PathLike = self._working_tree_dir or self.common_dir
312-
self.git = self.GitCommandWrapperType(self.working_dir)
329+
self.git = self.GitCommandWrapperType(self.working_dir, safe)
313330

314331
# Special handling, in special times.
315332
rootpath = osp.join(self.common_dir, "objects")
@@ -1305,6 +1322,7 @@ def init(
13051322
mkdir: bool = True,
13061323
odbt: Type[GitCmdObjectDB] = GitCmdObjectDB,
13071324
expand_vars: bool = True,
1325+
safe: bool = False,
13081326
**kwargs: Any,
13091327
) -> "Repo":
13101328
"""Initialize a git repository at the given path if specified.
@@ -1329,6 +1347,8 @@ def init(
13291347
information disclosure, allowing attackers to access the contents of
13301348
environment variables.
13311349
1350+
TODO :param safe:
1351+
13321352
:param kwargs:
13331353
Keyword arguments serving as additional options to the
13341354
:manpage:`git-init(1)` command.
@@ -1342,9 +1362,9 @@ def init(
13421362
os.makedirs(path, 0o755)
13431363

13441364
# git command automatically chdir into the directory
1345-
git = cls.GitCommandWrapperType(path)
1365+
git = cls.GitCommandWrapperType(path, safe)
13461366
git.init(**kwargs)
1347-
return cls(path, odbt=odbt)
1367+
return cls(path, odbt=odbt, safe=safe)
13481368

13491369
@classmethod
13501370
def _clone(
@@ -1357,6 +1377,7 @@ def _clone(
13571377
multi_options: Optional[List[str]] = None,
13581378
allow_unsafe_protocols: bool = False,
13591379
allow_unsafe_options: bool = False,
1380+
safe: Union[bool, None] = None,
13601381
**kwargs: Any,
13611382
) -> "Repo":
13621383
odbt = kwargs.pop("odbt", odb_default_type)
@@ -1418,7 +1439,11 @@ def _clone(
14181439
if not osp.isabs(path):
14191440
path = osp.join(git._working_dir, path) if git._working_dir is not None else path
14201441

1421-
repo = cls(path, odbt=odbt)
1442+
# if safe is not explicitly defined, then the new Repo instance should inherit the safe value
1443+
if safe is None:
1444+
safe = git._safe
1445+
1446+
repo = cls(path, odbt=odbt, safe=safe)
14221447

14231448
# Retain env values that were passed to _clone().
14241449
repo.git.update_environment(**git.environment())
@@ -1501,6 +1526,7 @@ def clone_from(
15011526
multi_options: Optional[List[str]] = None,
15021527
allow_unsafe_protocols: bool = False,
15031528
allow_unsafe_options: bool = False,
1529+
safe: bool = False,
15041530
**kwargs: Any,
15051531
) -> "Repo":
15061532
"""Create a clone from the given URL.
@@ -1531,13 +1557,16 @@ def clone_from(
15311557
:param allow_unsafe_options:
15321558
Allow unsafe options to be used, like ``--upload-pack``.
15331559
1560+
:param safe:
1561+
TODO
1562+
15341563
:param kwargs:
15351564
See the :meth:`clone` method.
15361565
15371566
:return:
15381567
:class:`Repo` instance pointing to the cloned directory.
15391568
"""
1540-
git = cls.GitCommandWrapperType(os.getcwd())
1569+
git = cls.GitCommandWrapperType(os.getcwd(), safe)
15411570
if env is not None:
15421571
git.update_environment(**env)
15431572
return cls._clone(
@@ -1549,6 +1578,7 @@ def clone_from(
15491578
multi_options,
15501579
allow_unsafe_protocols=allow_unsafe_protocols,
15511580
allow_unsafe_options=allow_unsafe_options,
1581+
safe=safe,
15521582
**kwargs,
15531583
)
15541584

0 commit comments

Comments
 (0)