Skip to content

Commit 509d0c5

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

File tree

2 files changed

+50
-6
lines changed

2 files changed

+50
-6
lines changed

git/cmd.py

Lines changed: 26 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,21 @@ 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', 'core.askpass=/bin/true',
1217+
'-c', 'core.hooksPath=/dev/null',
1218+
'-c', 'core.sshCommand=/bin/true',
1219+
'-c', 'credential.helper=/bin/true',
1220+
'-c', 'http.emptyAuth=true',
1221+
'-c', 'protocol.allow=never',
1222+
'-c', 'protocol.https.allow=always',
1223+
'-c', 'url.https://.insteadOf=ssh://',
1224+
]
1225+
command = [command.pop(0)] + config_args + command
1226+
12081227
# Remove password for the command if present.
12091228
redacted_command = remove_password_if_present(command)
12101229
if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process):
@@ -1227,6 +1246,12 @@ def execute(
12271246
# just to be sure.
12281247
env["LANGUAGE"] = "C"
12291248
env["LC_ALL"] = "C"
1249+
# Globally disable things that can execute commands, including password prompts.
1250+
if self._safe:
1251+
env['GIT_TERMINAL_PROMPT'] = 'false'
1252+
env['GIT_ASKPASS'] = '/bin/true'
1253+
env['SSH_ASKPASS'] = '/bin/true'
1254+
env['GIT_SSH'] = '/bin/true'
12301255
env.update(self._environment)
12311256
if inline_env is not None:
12321257
env.update(inline_env)

git/repo/base.py

Lines changed: 24 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,11 @@ 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.
215+
207216
:raise git.exc.InvalidGitRepositoryError:
208217
209218
:raise git.exc.NoSuchPathError:
@@ -235,6 +244,8 @@ def __init__(
235244
if not os.path.exists(epath):
236245
raise NoSuchPathError(epath)
237246

247+
self.safe = safe
248+
238249
# Walk up the path to find the `.git` dir.
239250
curpath = epath
240251
git_dir = None
@@ -289,6 +300,8 @@ def __init__(
289300
raise InvalidGitRepositoryError(epath)
290301
self.git_dir = git_dir
291302

303+
self.safe = safe
304+
292305
self._bare = False
293306
try:
294307
self._bare = self.config_reader("repository").getboolean("core", "bare")
@@ -309,7 +322,7 @@ def __init__(
309322
# END working dir handling
310323

311324
self.working_dir: PathLike = self._working_tree_dir or self.common_dir
312-
self.git = self.GitCommandWrapperType(self.working_dir)
325+
self.git = self.GitCommandWrapperType(self.working_dir, safe)
313326

314327
# Special handling, in special times.
315328
rootpath = osp.join(self.common_dir, "objects")
@@ -1305,6 +1318,7 @@ def init(
13051318
mkdir: bool = True,
13061319
odbt: Type[GitCmdObjectDB] = GitCmdObjectDB,
13071320
expand_vars: bool = True,
1321+
safe: bool = False,
13081322
**kwargs: Any,
13091323
) -> "Repo":
13101324
"""Initialize a git repository at the given path if specified.
@@ -1329,6 +1343,8 @@ def init(
13291343
information disclosure, allowing attackers to access the contents of
13301344
environment variables.
13311345
1346+
TODO :param safe:
1347+
13321348
:param kwargs:
13331349
Keyword arguments serving as additional options to the
13341350
:manpage:`git-init(1)` command.
@@ -1342,9 +1358,9 @@ def init(
13421358
os.makedirs(path, 0o755)
13431359

13441360
# git command automatically chdir into the directory
1345-
git = cls.GitCommandWrapperType(path)
1361+
git = cls.GitCommandWrapperType(path, safe)
13461362
git.init(**kwargs)
1347-
return cls(path, odbt=odbt)
1363+
return cls(path, odbt=odbt, safe=safe)
13481364

13491365
@classmethod
13501366
def _clone(
@@ -1357,6 +1373,7 @@ def _clone(
13571373
multi_options: Optional[List[str]] = None,
13581374
allow_unsafe_protocols: bool = False,
13591375
allow_unsafe_options: bool = False,
1376+
safe: bool = False,
13601377
**kwargs: Any,
13611378
) -> "Repo":
13621379
odbt = kwargs.pop("odbt", odb_default_type)
@@ -1418,7 +1435,7 @@ def _clone(
14181435
if not osp.isabs(path):
14191436
path = osp.join(git._working_dir, path) if git._working_dir is not None else path
14201437

1421-
repo = cls(path, odbt=odbt)
1438+
repo = cls(path, odbt=odbt, safe=safe)
14221439

14231440
# Retain env values that were passed to _clone().
14241441
repo.git.update_environment(**git.environment())
@@ -1501,6 +1518,7 @@ def clone_from(
15011518
multi_options: Optional[List[str]] = None,
15021519
allow_unsafe_protocols: bool = False,
15031520
allow_unsafe_options: bool = False,
1521+
safe: bool = False,
15041522
**kwargs: Any,
15051523
) -> "Repo":
15061524
"""Create a clone from the given URL.
@@ -1537,7 +1555,7 @@ def clone_from(
15371555
:return:
15381556
:class:`Repo` instance pointing to the cloned directory.
15391557
"""
1540-
git = cls.GitCommandWrapperType(os.getcwd())
1558+
git = cls.GitCommandWrapperType(os.getcwd(), safe)
15411559
if env is not None:
15421560
git.update_environment(**env)
15431561
return cls._clone(
@@ -1549,6 +1567,7 @@ def clone_from(
15491567
multi_options,
15501568
allow_unsafe_protocols=allow_unsafe_protocols,
15511569
allow_unsafe_options=allow_unsafe_options,
1570+
safe=safe,
15521571
**kwargs,
15531572
)
15541573

0 commit comments

Comments
 (0)