| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 2d7f566 commit 97eadb8
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,6 +2,19 @@ | |||
| 2 | 2 | Changelog | |
| 3 | 3 | ========= | |
| 4 | 4 | ||
| 5 | + 3.2.1 | ||
| 6 | + ===== | ||
| 7 | + | ||
| 8 | + Security fixes for | ||
| 9 | + | ||
| 10 | + * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-83vg-56qc-22m7 | ||
| 11 | + | ||
| 12 | + If you can, also try and provide feedback on the upcoming v4 branch | ||
| 13 | + https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome. | ||
| 14 | + | ||
| 15 | + See the following for all changes. | ||
| 16 | + https://github.com/gitpython-developers/GitPython/releases/tag/3.2.1 | ||
| 17 | + | ||
| 5 | 18 | 3.2.0 | |
| 6 | 19 | ===== | |
| 7 | 20 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -8,6 +8,7 @@ | |||
| 8 | 8 | import ntpath | |
| 9 | 9 | import os | |
| 10 | 10 | import os.path as osp | |
| 11 | + import re | ||
| 11 | 12 | import shlex | |
| 12 | 13 | import stat | |
| 13 | 14 | import sys | |
@@ -47,6 +48,7 @@ | |||
| 47 | 48 | IterableList, | |
| 48 | 49 | RemoteProgress, | |
| 49 | 50 | _to_relative_path, | |
| 51 | + _validate_repo_path, | ||
| 50 | 52 | join_path_native, | |
| 51 | 53 | rmtree, | |
| 52 | 54 | to_native_path_linux, | |
@@ -305,20 +307,55 @@ def _config_parser_constrained(self, read_only: bool) -> SectionConstraint: | |||
| 305 | 307 | def _validated_name(cls, name: str) -> str: | |
| 306 | 308 | if ( | |
| 307 | 309 | not name | |
| 310 | + or "\0" in name | ||
| 308 | 311 | or name.startswith(("/", "\\")) | |
| 309 | 312 | or ntpath.splitdrive(name)[0] | |
| 310 | 313 | or ".." in name.replace("\\", "/").split("/") | |
| 311 | 314 | ): | |
| 312 | 315 | raise ValueError("Invalid submodule name %r" % name) | |
| 316 | + cls._validate_windows_path(name) | ||
| 313 | 317 | return name | |
| 314 | 318 | ||
| 319 | + @staticmethod | ||
| 320 | + def _validate_windows_path(path: PathLike) -> None: | ||
| 321 | + """Apply Git for Windows' filename checks before creating directories.""" | ||
| 322 | + if sys.platform == "win32": | ||
| 323 | + for component in ntpath.splitdrive(os.fspath(path))[1].replace("\\", "/").split("/"): | ||
| 324 | + if component in (".", ".."): | ||
| 325 | + continue | ||
| 326 | + stem = component.split(".", 1)[0].rstrip(" ").upper() | ||
| 327 | + if ( | ||
| 328 | + component.endswith((" ", ".")) | ||
| 329 | + or any(ord(char) < 32 or char in '<>:"|?*' for char in component) | ||
| 330 | + or re.fullmatch(r"CON(?:IN\$|OUT\$)?|PRN|AUX|NUL|COM[1-9]|LPT[1-9]", stem) | ||
| 331 | + ): | ||
| 332 | + raise ValueError("Invalid submodule path on Windows: %r" % path) | ||
| 333 | + | ||
| 315 | 334 | @classmethod | |
| 316 | - def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> PathLike: | ||
| 335 | + def _module_abspath( | ||
| 336 | + cls, parent_repo: "Repo", path: PathLike, name: str, *, moving_from: Union[PathLike, None] = None | ||
| 337 | + ) -> PathLike: | ||
| 338 | + """Reject nested Git directories, allowing the source of a pending rename.""" | ||
| 339 | + from git.repo.fun import is_git_dir | ||
| 340 | + | ||
| 317 | 341 | name = cls._validated_name(name) | |
| 318 | 342 | if cls._need_gitfile_submodules(parent_repo.git): | |
| 343 | + directory = osp.join(parent_repo.git_dir, "modules") | ||
| 344 | + for component in to_native_path_linux(name).split("/")[:-1]: | ||
| 345 | + directory = osp.join(directory, component) | ||
| 346 | + if is_git_dir(directory) and ( | ||
| 347 | + moving_from is None or Path(directory).resolve() != Path(moving_from).resolve() | ||
| 348 | + ): | ||
| 349 | + raise ValueError( | ||
| 350 | + "Submodule metadata for %r is inside another Git directory: %r" % (name, directory) | ||
| 351 | + ) | ||
| 319 | 352 | return osp.join(parent_repo.git_dir, "modules", name) | |
| 320 | 353 | if parent_repo.working_tree_dir: | |
| 321 | - return cls._checked_abspath(parent_repo.working_tree_dir, cls._to_relative_path(parent_repo, path)) | ||
| 354 | + return cls._checked_abspath( | ||
| 355 | + parent_repo.working_tree_dir, | ||
| 356 | + cls._to_relative_path(parent_repo, path), | ||
| 357 | + git_dirs=(parent_repo.git_dir, parent_repo.common_dir), | ||
| 358 | + ) | ||
| 322 | 359 | raise NotADirectoryError() | |
| 323 | 360 | ||
| 324 | 361 | @classmethod | |
@@ -360,7 +397,9 @@ def _clone_repo( | |||
| 360 | 397 | path = cls._to_relative_path(repo, path) | |
| 361 | 398 | if repo.working_tree_dir is None: | |
| 362 | 399 | raise NotADirectoryError("Submodules require a working tree") | |
| 363 | - module_checkout_path = cls._checked_abspath(repo.working_tree_dir, path) | ||
| 400 | + module_checkout_path = cls._checked_abspath( | ||
| 401 | + repo.working_tree_dir, path, git_dirs=(repo.git_dir, repo.common_dir) | ||
| 402 | + ) | ||
| 364 | 403 | module_abspath = cls._module_abspath(repo, path, name) | |
| 365 | 404 | if cls._need_gitfile_submodules(repo.git): | |
| 366 | 405 | if not allow_unsafe_options: | |
@@ -402,6 +441,16 @@ def _clone_repo( | |||
| 402 | 441 | **kwargs, | |
| 403 | 442 | ) | |
| 404 | 443 | if cls._need_gitfile_submodules(repo.git): | |
| 444 | + # A concurrent clone may have turned a leading directory into a repository. | ||
| 445 | + try: | ||
| 446 | + cls._module_abspath(repo, path, name) | ||
| 447 | + except ValueError: | ||
| 448 | + clone.close() | ||
| 449 | + try: | ||
| 450 | + os.remove(osp.join(clone.git_dir, "HEAD")) | ||
| 451 | + except FileNotFoundError: | ||
| 452 | + pass | ||
| 453 | + raise | ||
| 405 | 454 | cls._write_git_file_and_module_config(module_checkout_path, module_abspath) | |
| 406 | 455 | ||
| 407 | 456 | return clone | |
@@ -411,8 +460,9 @@ def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike: | |||
| 411 | 460 | """:return: A path guaranteed to be relative to the given parent repository | |
| 412 | 461 | ||
| 413 | 462 | :raise ValueError: | |
| 414 | - If path is not contained in the parent repository's working tree. | ||
| 463 | + If path is outside the working tree or is unsafe as a submodule checkout. | ||
| 415 | 464 | """ | |
| 465 | + cls._validate_windows_path(path) | ||
| 416 | 466 | if parent_repo.working_tree_dir: | |
| 417 | 467 | path = _to_relative_path(parent_repo.working_tree_dir, path) | |
| 418 | 468 | else: | |
@@ -422,6 +472,7 @@ def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike: | |||
| 422 | 472 | if not path or path == ".": | |
| 423 | 473 | raise ValueError("Submodule checkout path must not be the repository root") | |
| 424 | 474 | ||
| 475 | + _validate_repo_path(path) | ||
| 425 | 476 | return path | |
| 426 | 477 | ||
| 427 | 478 | @property | |
@@ -433,22 +484,35 @@ def abspath(self) -> PathLike: | |||
| 433 | 484 | ||
| 434 | 485 | def _checkout_abspath(self, relative_path: PathLike, allow_final_symlink: bool = False) -> PathLike: | |
| 435 | 486 | """Check a checkout path already normalized by :meth:`_to_relative_path`.""" | |
| 436 | - return self._checked_abspath(self.repo.working_tree_dir, relative_path, allow_final_symlink) | ||
| 487 | + return self._checked_abspath( | ||
| 488 | + self.repo.working_tree_dir, | ||
| 489 | + relative_path, | ||
| 490 | + allow_final_symlink, | ||
| 491 | + git_dirs=(self.repo.git_dir, self.repo.common_dir), | ||
| 492 | + ) | ||
| 437 | 493 | ||
| 438 | 494 | @classmethod | |
| 439 | 495 | def _checked_abspath( | |
| 440 | - cls, root: Union[PathLike, None], relative_path: PathLike, allow_final_symlink: bool = False | ||
| 496 | + cls, | ||
| 497 | + root: Union[PathLike, None], | ||
| 498 | + relative_path: PathLike, | ||
| 499 | + allow_final_symlink: bool = False, | ||
| 500 | + *, | ||
| 501 | + git_dirs: Sequence[PathLike] = (), | ||
| 441 | 502 | ) -> str: | |
| 442 | - """Reject symlinks below a trusted root before accessing submodule paths.""" | ||
| 503 | + """Reject symlinks and checkout aliases of Git directories below a trusted root.""" | ||
| 443 | 504 | if root is None: | |
| 444 | 505 | raise NotADirectoryError("Submodules require a working tree") | |
| 445 | 506 | path = os.fspath(root) | |
| 507 | + metadata_dirs = set(git_dirs) | ||
| 446 | 508 | components = to_native_path_linux(relative_path).split("/") | |
| 447 | 509 | for index, component in enumerate(components): | |
| 448 | 510 | path = os.fspath(join_path_native(path, component)) | |
| 449 | - if allow_final_symlink and index == len(components) - 1: | ||
| 450 | - break | ||
| 511 | + if metadata_dirs and osp.exists(path) and any(osp.samefile(path, directory) for directory in metadata_dirs): | ||
| 512 | + raise ValueError("Submodule checkout path aliases Git metadata: %r" % relative_path) | ||
| 451 | 513 | if osp.islink(path): | |
| 514 | + if allow_final_symlink and index == len(components) - 1: | ||
| 515 | + break | ||
| 452 | 516 | raise ValueError("Submodule path %r contains a symbolic link" % relative_path) | |
| 453 | 517 | return path | |
| 454 | 518 | ||
@@ -1122,6 +1186,11 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = | |||
| 1122 | 1186 | self._checked_abspath(self.repo.working_tree_dir, self.k_modules_file) | |
| 1123 | 1187 | # Validate the source before removing the destination. | |
| 1124 | 1188 | cur_path = self.abspath | |
| 1189 | + module_abspath = self._module_abspath(self.repo, self.path, self.name) | ||
| 1190 | + if self.path == self.name: | ||
| 1191 | + self._module_abspath( | ||
| 1192 | + self.repo, module_checkout_path, os.fspath(module_checkout_path), moving_from=module_abspath | ||
| 1193 | + ) | ||
| 1125 | 1194 | module_checkout_abspath = self._checkout_abspath(module_checkout_path, allow_final_symlink=True) | |
| 1126 | 1195 | if osp.isfile(module_checkout_abspath): | |
| 1127 | 1196 | raise ValueError("Cannot move repository onto a file: %s" % module_checkout_abspath) | |
@@ -1160,7 +1229,6 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = | |||
| 1160 | 1229 | renamed_module = True | |
| 1161 | 1230 | ||
| 1162 | 1231 | if osp.isfile(osp.join(module_checkout_abspath, ".git")): | |
| 1163 | - module_abspath = self._module_abspath(self.repo, self.path, self.name) | ||
| 1164 | 1232 | self._write_git_file_and_module_config(module_checkout_abspath, module_abspath) | |
| 1165 | 1233 | # END handle git file rewrite | |
| 1166 | 1234 | # END move physical module | |
@@ -1522,8 +1590,8 @@ def rename(self, new_name: str) -> "Submodule": | |||
| 1522 | 1590 | ||
| 1523 | 1591 | self._validated_name(self.name) | |
| 1524 | 1592 | self._validated_name(new_name) | |
| 1525 | - destination_module_abspath = self._module_abspath(self.repo, self.path, new_name) | ||
| 1526 | 1593 | mod = self.module() | |
| 1594 | + destination_module_abspath = self._module_abspath(self.repo, self.path, new_name, moving_from=mod.git_dir) | ||
| 1527 | 1595 | self._checked_abspath(self.repo.working_tree_dir, self.k_modules_file) | |
| 1528 | 1596 | ||
| 1529 | 1597 | # .git/config | |
@@ -1573,6 +1641,7 @@ def module(self) -> "Repo": | |||
| 1573 | 1641 | """ | |
| 1574 | 1642 | self._validated_name(self.name) | |
| 1575 | 1643 | module_checkout_abspath = self.abspath | |
| 1644 | + self._module_abspath(self.repo, self.path, self.name) | ||
| 1576 | 1645 | try: | |
| 1577 | 1646 | repo = git.Repo(module_checkout_abspath) | |
| 1578 | 1647 | if repo != self.repo: | |
| Back | FazBrowse Home | New Git URL |
0 commit comments