| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -26,6 +26,7 @@ | |||
| 26 | 26 | CommandError, | |
| 27 | 27 | GitCommandError, | |
| 28 | 28 | GitCommandNotFound, | |
| 29 | + UnsafeExecutionError, | ||
| 29 | 30 | UnsafeOptionError, | |
| 30 | 31 | UnsafeProtocolError, | |
| 31 | 32 | ) | |
@@ -627,6 +628,7 @@ class Git(metaclass=_GitMeta): | |||
| 627 | 628 | ||
| 628 | 629 | __slots__ = ( | |
| 629 | 630 | "_working_dir", | |
| 631 | + "_safe", | ||
| 630 | 632 | "cat_file_all", | |
| 631 | 633 | "cat_file_header", | |
| 632 | 634 | "_version_info", | |
@@ -961,17 +963,56 @@ def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> | |||
| 961 | 963 | ||
| 962 | 964 | CatFileContentStream: TypeAlias = _CatFileContentStream | |
| 963 | 965 | ||
| 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: | ||
| 965 | 967 | """Initialize this instance with: | |
| 966 | 968 | ||
| 967 | 969 | :param working_dir: | |
| 968 | 970 | Git directory we should work in. If ``None``, we always work in the current | |
| 969 | 971 | directory as returned by :func:`os.getcwd`. | |
| 970 | 972 | This is meant to be the working tree directory if available, or the | |
| 971 | 973 | ``.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``. | ||
| 972 | 1012 | """ | |
| 973 | 1013 | super().__init__() | |
| 974 | 1014 | self._working_dir = expand_path(working_dir) | |
| 1015 | + self._safe = safe | ||
| 975 | 1016 | self._git_options: Union[List[str], Tuple[str, ...]] = () | |
| 976 | 1017 | self._persistent_git_options: List[str] = [] | |
| 977 | 1018 | ||
@@ -1218,6 +1259,8 @@ def execute( | |||
| 1218 | 1259 | ||
| 1219 | 1260 | :raise git.exc.GitCommandError: | |
| 1220 | 1261 | ||
| 1262 | + :raise git.exc.UnsafeExecutionError: | ||
| 1263 | + | ||
| 1221 | 1264 | :note: | |
| 1222 | 1265 | If you add additional keyword arguments to the signature of this method, you | |
| 1223 | 1266 | must update the ``execute_kwargs`` variable housed in this module. | |
@@ -1227,6 +1270,64 @@ def execute( | |||
| 1227 | 1270 | if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process): | |
| 1228 | 1271 | _logger.info(" ".join(redacted_command)) | |
| 1229 | 1272 | ||
| 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 | + | ||
| 1230 | 1331 | # Allow the user to have the command executed in their working dir. | |
| 1231 | 1332 | try: | |
| 1232 | 1333 | cwd = self._working_dir or os.getcwd() # type: Union[None, str] | |
@@ -1244,6 +1345,15 @@ def execute( | |||
| 1244 | 1345 | # just to be sure. | |
| 1245 | 1346 | env["LANGUAGE"] = "C" | |
| 1246 | 1347 | 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" | ||
| 1247 | 1357 | env.update(self._environment) | |
| 1248 | 1358 | if inline_env is not None: | |
| 1249 | 1359 | env.update(inline_env) | |
@@ -1260,13 +1370,6 @@ def execute( | |||
| 1260 | 1370 | # END handle | |
| 1261 | 1371 | ||
| 1262 | 1372 | 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") | ||
| 1270 | 1373 | _logger.debug( | |
| 1271 | 1374 | "Popen(%s, cwd=%s, stdin=%s, shell=%s, universal_newlines=%s)", | |
| 1272 | 1375 | redacted_command, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -159,6 +159,10 @@ def __init__( | |||
| 159 | 159 | super().__init__(command, status, stderr, stdout) | |
| 160 | 160 | ||
| 161 | 161 | ||
| 162 | + class UnsafeExecutionError(CommandError): | ||
| 163 | + """Thrown if anything but git is executed when in safe mode.""" | ||
| 164 | + | ||
| 165 | + | ||
| 162 | 166 | class CheckoutError(GitError): | |
| 163 | 167 | """Thrown if a file could not be checked out from the index as it contained | |
| 164 | 168 | changes. | |
| Back | FazBrowse Home | New Git URL |
0 commit comments