| 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 | ) | |
@@ -398,6 +399,7 @@ class Git(metaclass=_GitMeta): | |||
| 398 | 399 | ||
| 399 | 400 | __slots__ = ( | |
| 400 | 401 | "_working_dir", | |
| 402 | + "_safe", | ||
| 401 | 403 | "cat_file_all", | |
| 402 | 404 | "cat_file_header", | |
| 403 | 405 | "_version_info", | |
@@ -944,17 +946,20 @@ def __del__(self) -> None: | |||
| 944 | 946 | self._stream.read(bytes_left + 1) | |
| 945 | 947 | # END handle incomplete read | |
| 946 | 948 | ||
| 947 | - def __init__(self, working_dir: Union[None, PathLike] = None) -> None: | ||
| 949 | + def __init__(self, working_dir: Union[None, PathLike] = None, safe: bool = False) -> None: | ||
| 948 | 950 | """Initialize this instance with: | |
| 949 | 951 | ||
| 950 | 952 | :param working_dir: | |
| 951 | 953 | Git directory we should work in. If ``None``, we always work in the current | |
| 952 | 954 | directory as returned by :func:`os.getcwd`. | |
| 953 | 955 | This is meant to be the working tree directory if available, or the | |
| 954 | 956 | ``.git`` directory in case of bare repositories. | |
| 957 | + | ||
| 958 | + TODO :param safe: | ||
| 955 | 959 | """ | |
| 956 | 960 | super().__init__() | |
| 957 | 961 | self._working_dir = expand_path(working_dir) | |
| 962 | + self._safe = safe | ||
| 958 | 963 | self._git_options: Union[List[str], Tuple[str, ...]] = () | |
| 959 | 964 | self._persistent_git_options: List[str] = [] | |
| 960 | 965 | ||
@@ -1201,6 +1206,8 @@ def execute( | |||
| 1201 | 1206 | ||
| 1202 | 1207 | :raise git.exc.GitCommandError: | |
| 1203 | 1208 | ||
| 1209 | + :raise git.exc.UnsafeExecutionError: | ||
| 1210 | + | ||
| 1204 | 1211 | :note: | |
| 1205 | 1212 | If you add additional keyword arguments to the signature of this method, you | |
| 1206 | 1213 | must update the ``execute_kwargs`` variable housed in this module. | |
@@ -1210,6 +1217,51 @@ def execute( | |||
| 1210 | 1217 | if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process): | |
| 1211 | 1218 | _logger.info(" ".join(redacted_command)) | |
| 1212 | 1219 | ||
| 1220 | + if self._safe: | ||
| 1221 | + if isinstance(command, str) or command[0] != self.GIT_PYTHON_GIT_EXECUTABLE: | ||
| 1222 | + raise UnsafeExecutionError( | ||
| 1223 | + redacted_command, | ||
| 1224 | + f'Only {self.GIT_PYTHON_GIT_EXECUTABLE} can be executed when in safe mode.', | ||
| 1225 | + ) | ||
| 1226 | + if shell: | ||
| 1227 | + raise UnsafeExecutionError( | ||
| 1228 | + redacted_command, | ||
| 1229 | + f'Command cannot be executed in a shell when in safe mode.', | ||
| 1230 | + ) | ||
| 1231 | + config_args = [ | ||
| 1232 | + "-c", | ||
| 1233 | + "core.askpass=/bin/true", | ||
| 1234 | + "-c", | ||
| 1235 | + "core.fsmonitor=false", | ||
| 1236 | + "-c", | ||
| 1237 | + "core.hooksPath=/dev/null", | ||
| 1238 | + "-c", | ||
| 1239 | + "core.sshCommand=/bin/true", | ||
| 1240 | + "-c", | ||
| 1241 | + "credential.helper=/bin/true", | ||
| 1242 | + "-c", | ||
| 1243 | + "http.emptyAuth=true", | ||
| 1244 | + "-c", | ||
| 1245 | + "protocol.allow=never", | ||
| 1246 | + "-c", | ||
| 1247 | + "protocol.https.allow=always", | ||
| 1248 | + "-c", | ||
| 1249 | + "url.https://bitbucket.org/.insteadOf=git@bitbucket.org:", | ||
| 1250 | + "-c", | ||
| 1251 | + "url.https://codeberg.org/.insteadOf=git@codeberg.org:", | ||
| 1252 | + "-c", | ||
| 1253 | + "url.https://github.com/.insteadOf=git@github.com:", | ||
| 1254 | + "-c", | ||
| 1255 | + "url.https://gitlab.com/.insteadOf=git@gitlab.com:", | ||
| 1256 | + "-c", | ||
| 1257 | + "url.https://.insteadOf=git://", | ||
| 1258 | + "-c", | ||
| 1259 | + "url.https://.insteadOf=http://", | ||
| 1260 | + "-c", | ||
| 1261 | + "url.https://.insteadOf=ssh://", | ||
| 1262 | + ] | ||
| 1263 | + command = [command.pop(0)] + config_args + command | ||
| 1264 | + | ||
| 1213 | 1265 | # Allow the user to have the command executed in their working dir. | |
| 1214 | 1266 | try: | |
| 1215 | 1267 | cwd = self._working_dir or os.getcwd() # type: Union[None, str] | |
@@ -1227,6 +1279,15 @@ def execute( | |||
| 1227 | 1279 | # just to be sure. | |
| 1228 | 1280 | env["LANGUAGE"] = "C" | |
| 1229 | 1281 | env["LC_ALL"] = "C" | |
| 1282 | + # Globally disable things that can execute commands, including password prompts. | ||
| 1283 | + if self._safe: | ||
| 1284 | + env["GIT_ASKPASS"] = "/bin/true" | ||
| 1285 | + env["GIT_EDITOR"] = "/bin/true" | ||
| 1286 | + env["GIT_PAGER"] = "/bin/true" | ||
| 1287 | + env["GIT_SSH"] = "/bin/true" | ||
| 1288 | + env["GIT_SSH_COMMAND"] = "/bin/true" | ||
| 1289 | + env["GIT_TERMINAL_PROMPT"] = "false" | ||
| 1290 | + env["SSH_ASKPASS"] = "/bin/true" | ||
| 1230 | 1291 | env.update(self._environment) | |
| 1231 | 1292 | if inline_env is not None: | |
| 1232 | 1293 | env.update(inline_env) | |
| 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. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -131,6 +131,9 @@ class Repo: | |||
| 131 | 131 | git_dir: PathLike | |
| 132 | 132 | """The ``.git`` repository directory.""" | |
| 133 | 133 | ||
| 134 | + safe: None | ||
| 135 | + """Whether this is operating using restricted protocol and execution access.""" | ||
| 136 | + | ||
| 134 | 137 | _common_dir: PathLike = "" | |
| 135 | 138 | ||
| 136 | 139 | # Precompiled regex | |
@@ -175,6 +178,7 @@ def __init__( | |||
| 175 | 178 | odbt: Type[LooseObjectDB] = GitCmdObjectDB, | |
| 176 | 179 | search_parent_directories: bool = False, | |
| 177 | 180 | expand_vars: bool = True, | |
| 181 | + safe: bool = False, | ||
| 178 | 182 | ) -> None: | |
| 179 | 183 | R"""Create a new :class:`Repo` instance. | |
| 180 | 184 | ||
@@ -204,6 +208,17 @@ def __init__( | |||
| 204 | 208 | Please note that this was the default behaviour in older versions of | |
| 205 | 209 | GitPython, which is considered a bug though. | |
| 206 | 210 | ||
| 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 | + | ||
| 207 | 222 | :raise git.exc.InvalidGitRepositoryError: | |
| 208 | 223 | ||
| 209 | 224 | :raise git.exc.NoSuchPathError: | |
@@ -235,6 +250,8 @@ def __init__( | |||
| 235 | 250 | if not os.path.exists(epath): | |
| 236 | 251 | raise NoSuchPathError(epath) | |
| 237 | 252 | ||
| 253 | + self.safe = safe | ||
| 254 | + | ||
| 238 | 255 | # Walk up the path to find the `.git` dir. | |
| 239 | 256 | curpath = epath | |
| 240 | 257 | git_dir = None | |
@@ -309,7 +326,7 @@ def __init__( | |||
| 309 | 326 | # END working dir handling | |
| 310 | 327 | ||
| 311 | 328 | 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) | ||
| 313 | 330 | ||
| 314 | 331 | # Special handling, in special times. | |
| 315 | 332 | rootpath = osp.join(self.common_dir, "objects") | |
@@ -1305,6 +1322,7 @@ def init( | |||
| 1305 | 1322 | mkdir: bool = True, | |
| 1306 | 1323 | odbt: Type[GitCmdObjectDB] = GitCmdObjectDB, | |
| 1307 | 1324 | expand_vars: bool = True, | |
| 1325 | + safe: bool = False, | ||
| 1308 | 1326 | **kwargs: Any, | |
| 1309 | 1327 | ) -> "Repo": | |
| 1310 | 1328 | """Initialize a git repository at the given path if specified. | |
@@ -1329,6 +1347,8 @@ def init( | |||
| 1329 | 1347 | information disclosure, allowing attackers to access the contents of | |
| 1330 | 1348 | environment variables. | |
| 1331 | 1349 | ||
| 1350 | + TODO :param safe: | ||
| 1351 | + | ||
| 1332 | 1352 | :param kwargs: | |
| 1333 | 1353 | Keyword arguments serving as additional options to the | |
| 1334 | 1354 | :manpage:`git-init(1)` command. | |
@@ -1342,9 +1362,9 @@ def init( | |||
| 1342 | 1362 | os.makedirs(path, 0o755) | |
| 1343 | 1363 | ||
| 1344 | 1364 | # git command automatically chdir into the directory | |
| 1345 | - git = cls.GitCommandWrapperType(path) | ||
| 1365 | + git = cls.GitCommandWrapperType(path, safe) | ||
| 1346 | 1366 | git.init(**kwargs) | |
| 1347 | - return cls(path, odbt=odbt) | ||
| 1367 | + return cls(path, odbt=odbt, safe=safe) | ||
| 1348 | 1368 | ||
| 1349 | 1369 | @classmethod | |
| 1350 | 1370 | def _clone( | |
@@ -1357,6 +1377,7 @@ def _clone( | |||
| 1357 | 1377 | multi_options: Optional[List[str]] = None, | |
| 1358 | 1378 | allow_unsafe_protocols: bool = False, | |
| 1359 | 1379 | allow_unsafe_options: bool = False, | |
| 1380 | + safe: Union[bool, None] = None, | ||
| 1360 | 1381 | **kwargs: Any, | |
| 1361 | 1382 | ) -> "Repo": | |
| 1362 | 1383 | odbt = kwargs.pop("odbt", odb_default_type) | |
@@ -1418,7 +1439,11 @@ def _clone( | |||
| 1418 | 1439 | if not osp.isabs(path): | |
| 1419 | 1440 | path = osp.join(git._working_dir, path) if git._working_dir is not None else path | |
| 1420 | 1441 | ||
| 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) | ||
| 1422 | 1447 | ||
| 1423 | 1448 | # Retain env values that were passed to _clone(). | |
| 1424 | 1449 | repo.git.update_environment(**git.environment()) | |
@@ -1501,6 +1526,7 @@ def clone_from( | |||
| 1501 | 1526 | multi_options: Optional[List[str]] = None, | |
| 1502 | 1527 | allow_unsafe_protocols: bool = False, | |
| 1503 | 1528 | allow_unsafe_options: bool = False, | |
| 1529 | + safe: bool = False, | ||
| 1504 | 1530 | **kwargs: Any, | |
| 1505 | 1531 | ) -> "Repo": | |
| 1506 | 1532 | """Create a clone from the given URL. | |
@@ -1531,13 +1557,16 @@ def clone_from( | |||
| 1531 | 1557 | :param allow_unsafe_options: | |
| 1532 | 1558 | Allow unsafe options to be used, like ``--upload-pack``. | |
| 1533 | 1559 | ||
| 1560 | + :param safe: | ||
| 1561 | + TODO | ||
| 1562 | + | ||
| 1534 | 1563 | :param kwargs: | |
| 1535 | 1564 | See the :meth:`clone` method. | |
| 1536 | 1565 | ||
| 1537 | 1566 | :return: | |
| 1538 | 1567 | :class:`Repo` instance pointing to the cloned directory. | |
| 1539 | 1568 | """ | |
| 1540 | - git = cls.GitCommandWrapperType(os.getcwd()) | ||
| 1569 | + git = cls.GitCommandWrapperType(os.getcwd(), safe) | ||
| 1541 | 1570 | if env is not None: | |
| 1542 | 1571 | git.update_environment(**env) | |
| 1543 | 1572 | return cls._clone( | |
@@ -1549,6 +1578,7 @@ def clone_from( | |||
| 1549 | 1578 | multi_options, | |
| 1550 | 1579 | allow_unsafe_protocols=allow_unsafe_protocols, | |
| 1551 | 1580 | allow_unsafe_options=allow_unsafe_options, | |
| 1581 | + safe=safe, | ||
| 1552 | 1582 | **kwargs, | |
| 1553 | 1583 | ) | |
| 1554 | 1584 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments