| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -373,22 +373,6 @@ | |||
| 373 | 373 | } | |
| 374 | 374 | ], | |
| 375 | 375 | "./git/repo/base.py": [ | |
| 376 | - { | ||
| 377 | - "code": "reportReturnType", | ||
| 378 | - "range": { | ||
| 379 | - "startColumn": 15, | ||
| 380 | - "endColumn": 46, | ||
| 381 | - "lineCount": 1 | ||
| 382 | - } | ||
| 383 | - }, | ||
| 384 | - { | ||
| 385 | - "code": "reportReturnType", | ||
| 386 | - "range": { | ||
| 387 | - "startColumn": 15, | ||
| 388 | - "endColumn": 51, | ||
| 389 | - "lineCount": 1 | ||
| 390 | - } | ||
| 391 | - }, | ||
| 392 | 376 | { | |
| 393 | 377 | "code": "reportReturnType", | |
| 394 | 378 | "range": { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,13 +1,6 @@ | |||
| 1 | 1 | exclude: ^(?:gitdb|smmap)/ | |
| 2 | 2 | ||
| 3 | 3 | repos: | |
| 4 | - - repo: https://github.com/codespell-project/codespell | ||
| 5 | - rev: v2.4.3 | ||
| 6 | - hooks: | ||
| 7 | - - id: codespell | ||
| 8 | - additional_dependencies: [tomli] | ||
| 9 | - exclude: ^test/fixtures/ | ||
| 10 | - | ||
| 11 | 4 | - repo: https://github.com/astral-sh/ruff-pre-commit | |
| 12 | 5 | rev: v0.16.5 | |
| 13 | 6 | hooks: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,6 +2,15 @@ | |||
| 2 | 2 | Changelog | |
| 3 | 3 | ========= | |
| 4 | 4 | ||
| 5 | + Unreleased | ||
| 6 | + ========== | ||
| 7 | + | ||
| 8 | + * Deprecate the pure-Python ``GitDB`` object database backend due to security and | ||
| 9 | + performance issues. Selecting it or a subclass through ``odbt`` now emits a | ||
| 10 | + ``DeprecationWarning``. Remove ``odbt=GitDB`` to use ``GitCmdObjectDB``, the | ||
| 11 | + existing default, or select ``odbt=GitCmdObjectDB`` explicitly. The ``gitdb`` | ||
| 12 | + package remains a dependency for shared types and utilities. | ||
| 13 | + | ||
| 5 | 14 | 3.2.1 | |
| 6 | 15 | ===== | |
| 7 | 16 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -513,24 +513,29 @@ Object Databases | |||
| 513 | 513 | ||
| 514 | 514 | The type of the database determines certain performance characteristics, such as the quantity of objects that can be read per second, the resource usage when reading large data files, as well as the average memory footprint of your application. | |
| 515 | 515 | ||
| 516 | - GitDB | ||
| 517 | - ===== | ||
| 518 | - The GitDB is a pure-python implementation of the git object database. It is the default database to use in GitPython 0.3. It uses less memory when handling huge files, but will be 2 to 5 times slower when extracting large quantities of small objects from densely packed repositories:: | ||
| 516 | + GitCmdObjectDB | ||
| 517 | + ============== | ||
| 518 | + ``GitCmdObjectDB`` is the default and recommended backend. It reads objects and | ||
| 519 | + resolves abbreviated object IDs through persistent ``git cat-file`` processes:: | ||
| 519 | 520 | ||
| 520 | - repo = Repo("path/to/repo", odbt=GitDB) | ||
| 521 | + repo = Repo("path/to/repo") | ||
| 522 | + # Equivalent explicit selection: | ||
| 523 | + repo = Repo("path/to/repo", odbt=GitCmdObjectDB) | ||
| 521 | 524 | ||
| 525 | + GitDB | ||
| 526 | + ===== | ||
| 522 | 527 | .. warning:: | |
| 523 | - ``GitDB`` may fail or become extremely slow when traversing trees in | ||
| 524 | - repositories with very large commits (thousands of changed files in a | ||
| 525 | - single commit). If you encounter ``RecursionError`` or excessive | ||
| 526 | - slowness during tree traversal, switch to ``GitCmdObjectDB`` instead. | ||
| 528 | + The pure-Python ``GitDB`` backend is deprecated due to security and performance | ||
| 529 | + issues. Its object parsers can exhaust resources or return incorrect object | ||
| 530 | + data when processing untrusted repositories. Do not use it for untrusted data. | ||
| 527 | 531 | ||
| 532 | + Selecting ``odbt=GitDB`` (including a subclass) emits a ``DeprecationWarning``. | ||
| 533 | + To migrate, remove ``odbt=GitDB`` or replace it with ``odbt=GitCmdObjectDB`` when | ||
| 534 | + opening, initializing, or cloning a repository. The deprecated backend remains | ||
| 535 | + available for compatibility; deprecation does not fix its parsing issues. | ||
| 528 | 536 | ||
| 529 | - GitCmdObjectDB | ||
| 530 | - ============== | ||
| 531 | - The git command database uses persistent git-cat-file instances to read repository information. These operate very fast under all conditions, but will consume additional memory for the process itself. When extracting large files, memory usage will be much higher than ``GitDB``:: | ||
| 532 | - | ||
| 533 | - repo = Repo("path/to/repo", odbt=GitCmdObjectDB) | ||
| 537 | + The ``gitdb`` package remains a dependency because GitPython still uses its shared | ||
| 538 | + types and utilities. | ||
| 534 | 539 | ||
| 535 | 540 | Git Command Debugging and Customization | |
| 536 | 541 | *************************************** | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -646,7 +646,8 @@ class Git(metaclass=_GitMeta): | |||
| 646 | 646 | ) | |
| 647 | 647 | ||
| 648 | 648 | # Match Git's leading transport selector, including an empty helper name. | |
| 649 | - re_unsafe_protocol = re.compile(r"([A-Za-z0-9][A-Za-z0-9+.-]*|)::") | ||
| 649 | + # Git also selects the command-executing ext helper for an ext:// URL. | ||
| 650 | + re_unsafe_protocol = re.compile(r"([A-Za-z0-9][A-Za-z0-9+.-]*|)::|ext://") | ||
| 650 | 651 | ||
| 651 | 652 | unsafe_git_ls_remote_options = [ | |
| 652 | 653 | # This option allows arbitrary command execution in git-ls-remote. | |
@@ -946,7 +947,8 @@ def check_unsafe_protocols(cls, url: str) -> None: | |||
| 946 | 947 | ||
| 947 | 948 | Apart from the usual protocols (http, git, ssh), Git allows "remote helpers" | |
| 948 | 949 | that have the form ``<transport>::<address>``. One of these helpers (``ext::``) | |
| 949 | - can be used to invoke any arbitrary command. | ||
| 950 | + can be used to invoke any arbitrary command. Git also selects that helper | ||
| 951 | + for ``ext://`` URLs and interprets the URL as a command path. | ||
| 950 | 952 | ||
| 951 | 953 | See: | |
| 952 | 954 | ||
@@ -955,11 +957,19 @@ def check_unsafe_protocols(cls, url: str) -> None: | |||
| 955 | 957 | """ | |
| 956 | 958 | match = cls.re_unsafe_protocol.match(url) | |
| 957 | 959 | if match: | |
| 958 | - protocol = match.group(1) | ||
| 960 | + protocol = match.group(0) | ||
| 959 | 961 | raise UnsafeProtocolError( | |
| 960 | - f"The `{protocol}::` protocol looks suspicious, use `allow_unsafe_protocols=True` to allow it." | ||
| 962 | + f"The `{protocol}` protocol looks suspicious, use `allow_unsafe_protocols=True` to allow it." | ||
| 961 | 963 | ) | |
| 962 | 964 | ||
| 965 | + def _check_unsafe_protocols_in_args(self, args: Sequence[Any], kwargs: Mapping[str, Any]) -> None: | ||
| 966 | + """Check positional operands and standalone values in rendered command options. | ||
| 967 | + | ||
| 968 | + A short flag's split value can become the repository operand before ``--``. | ||
| 969 | + """ | ||
| 970 | + for arg in self._unpack_args(args) + self.transform_kwargs(**kwargs): | ||
| 971 | + self.check_unsafe_protocols(arg) | ||
| 972 | + | ||
| 963 | 973 | @classmethod | |
| 964 | 974 | def _canonicalize_option_name(cls, option: str) -> str: | |
| 965 | 975 | """Return the option name used for unsafe-option checks. | |
@@ -1146,12 +1156,7 @@ def ls_remote( | |||
| 1146 | 1156 | candidate_options = self._option_candidates(args, kwargs) | |
| 1147 | 1157 | Git.check_unsafe_options(options=candidate_options, unsafe_options=self.unsafe_git_ls_remote_options) | |
| 1148 | 1158 | if not allow_unsafe_protocols: | |
| 1149 | - protocol_args = list(args) | ||
| 1150 | - if kwargs.get("split_single_char_options", True): | ||
| 1151 | - # Split short-option values can become the URL after parsing earlier options. | ||
| 1152 | - protocol_args.extend(value for key, value in kwargs.items() if len(key) == 1) | ||
| 1153 | - for arg in self._unpack_args(protocol_args): | ||
| 1154 | - self.check_unsafe_protocols(arg) | ||
| 1159 | + self._check_unsafe_protocols_in_args(args, kwargs) | ||
| 1155 | 1160 | return self._call_process("ls_remote", *args, **kwargs) | |
| 1156 | 1161 | ||
| 1157 | 1162 | @property | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,7 +14,7 @@ | |||
| 14 | 14 | from io import BytesIO | |
| 15 | 15 | import os | |
| 16 | 16 | import os.path as osp | |
| 17 | - from stat import S_ISLNK | ||
| 17 | + from stat import S_ISLNK, S_ISREG | ||
| 18 | 18 | import subprocess | |
| 19 | 19 | import sys | |
| 20 | 20 | import tempfile | |
@@ -36,6 +36,7 @@ | |||
| 36 | 36 | file_contents_ro, | |
| 37 | 37 | _is_path_rooted, | |
| 38 | 38 | _to_relative_path, | |
| 39 | + _validate_repo_path, | ||
| 39 | 40 | to_native_path_linux, | |
| 40 | 41 | unbare_repo, | |
| 41 | 42 | to_bin_sha, | |
@@ -476,7 +477,17 @@ def raise_exc(e: Exception) -> NoReturn: | |||
| 476 | 477 | continue | |
| 477 | 478 | # END glob handling | |
| 478 | 479 | try: | |
| 479 | - for root, _dirs, files in os.walk(abs_path, onerror=raise_exc): | ||
| 480 | + for root, dirs, files in os.walk(abs_path, onerror=raise_exc): | ||
| 481 | + for dirname in dirs[:]: | ||
| 482 | + directory = osp.join(root, dirname) | ||
| 483 | + try: | ||
| 484 | + _validate_repo_path(to_native_path_linux(osp.relpath(directory, r))) | ||
| 485 | + except ValueError: | ||
| 486 | + dirs.remove(dirname) | ||
| 487 | + continue | ||
| 488 | + if osp.islink(directory): | ||
| 489 | + dirs.remove(dirname) | ||
| 490 | + yield osp.relpath(directory, r) | ||
| 480 | 491 | for rela_file in files: | |
| 481 | 492 | # Add relative paths only. | |
| 482 | 493 | yield osp.join(root.replace(rs, ""), rela_file) | |
@@ -717,6 +728,8 @@ def _preprocess_add_items( | |||
| 717 | 728 | else: | |
| 718 | 729 | raise TypeError("Invalid Type: %r" % item) | |
| 719 | 730 | # END for each item | |
| 731 | + for entry in entries: | ||
| 732 | + _validate_repo_path(entry.path) | ||
| 720 | 733 | return paths, entries | |
| 721 | 734 | ||
| 722 | 735 | def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry: | |
@@ -726,20 +739,43 @@ def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry | |||
| 726 | 739 | This needs the :func:`~git.index.util.git_working_dir` decorator active! | |
| 727 | 740 | This must be ensured in the calling code. | |
| 728 | 741 | """ | |
| 729 | - st = os.lstat(filepath) # Handles non-symlinks as well. | ||
| 730 | - | ||
| 742 | + filepath = self._to_relative_path(filepath) | ||
| 743 | + _validate_repo_path(filepath) | ||
| 744 | + parent = osp.realpath(self.repo.working_dir) | ||
| 745 | + for component in os.fspath(filepath).split("/")[:-1]: | ||
| 746 | + parent = osp.join(parent, component) | ||
| 747 | + if osp.islink(parent) or osp.normcase(osp.realpath(parent)) != osp.normcase(osp.abspath(parent)): | ||
| 748 | + raise ValueError("Cannot stage a path beyond a symbolic link: %r" % filepath) | ||
| 749 | + st = os.lstat(filepath) | ||
| 750 | + if not S_ISLNK(st.st_mode) and not S_ISREG(st.st_mode): | ||
| 751 | + raise ValueError("Can only stage a regular file or symbolic link: %r" % filepath) | ||
| 752 | + | ||
| 753 | + stream_size = st.st_size | ||
| 731 | 754 | if S_ISLNK(st.st_mode): | |
| 732 | 755 | # readlink is a string, but we need bytes. | |
| 756 | + target = force_bytes(os.readlink(filepath), encoding=defenc) | ||
| 757 | + stream_size = len(target) | ||
| 758 | + | ||
| 733 | 759 | def open_stream() -> BinaryIO: | |
| 734 | - return BytesIO(force_bytes(os.readlink(filepath), encoding=defenc)) | ||
| 760 | + return BytesIO(target) | ||
| 735 | 761 | else: | |
| 736 | 762 | ||
| 737 | 763 | def open_stream() -> BinaryIO: | |
| 738 | - return open(filepath, "rb") | ||
| 764 | + # Do not follow a final symlink or block on a FIFO substituted | ||
| 765 | + # between lstat and open on platforms supporting these flags. | ||
| 766 | + def opener(path: str, flags: int) -> int: | ||
| 767 | + return os.open(path, flags | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)) | ||
| 768 | + | ||
| 769 | + return open(filepath, "rb", opener=opener) | ||
| 739 | 770 | ||
| 740 | 771 | with open_stream() as stream: | |
| 772 | + if not S_ISLNK(st.st_mode): | ||
| 773 | + st = os.fstat(stream.fileno()) | ||
| 774 | + if not S_ISREG(st.st_mode): | ||
| 775 | + raise ValueError("Can only stage a regular file: %r" % filepath) | ||
| 776 | + stream_size = st.st_size | ||
| 741 | 777 | fprogress(filepath, False, filepath) | |
| 742 | - istream = self.repo.odb.store(IStream(Blob.type, st.st_size, stream)) | ||
| 778 | + istream = self.repo.odb.store(IStream(Blob.type, stream_size, stream)) | ||
| 743 | 779 | fprogress(filepath, True, filepath) | |
| 744 | 780 | return BaseIndexEntry( | |
| 745 | 781 | ( | |
@@ -777,7 +813,7 @@ def _entries_for_paths( | |||
| 777 | 813 | blob = Blob( | |
| 778 | 814 | self.repo, | |
| 779 | 815 | Blob.NULL_BIN_SHA, | |
| 780 | - stat_mode_to_index_mode(os.stat(abspath).st_mode), | ||
| 816 | + stat_mode_to_index_mode(os.lstat(abspath).st_mode), | ||
| 781 | 817 | to_native_path_linux(gitrelative_path), | |
| 782 | 818 | ) | |
| 783 | 819 | # TODO: variable undefined | |
@@ -989,6 +1025,8 @@ def handle_null_entries(self: "IndexFile") -> None: | |||
| 989 | 1025 | ||
| 990 | 1026 | # FINALIZE | |
| 991 | 1027 | # Add the new entries to this instance. | |
| 1028 | + for entry in entries_added: | ||
| 1029 | + _validate_repo_path(entry.path) | ||
| 992 | 1030 | for entry in entries_added: | |
| 993 | 1031 | self.entries[(entry.path, 0)] = IndexEntry.from_base(entry) | |
| 994 | 1032 | ||
@@ -1390,6 +1428,9 @@ def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLik | |||
| 1390 | 1428 | ||
| 1391 | 1429 | # END stderr handler | |
| 1392 | 1430 | ||
| 1431 | + # Read and validate the index before Git trusts its paths for checkout. | ||
| 1432 | + self._delete_entries_cache() | ||
| 1433 | + self.entries # noqa: B018 | ||
| 1393 | 1434 | if paths is None: | |
| 1394 | 1435 | args.append("--all") | |
| 1395 | 1436 | kwargs["as_process"] = 1 | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -34,7 +34,7 @@ | |||
| 34 | 34 | traverse_trees_recursive, | |
| 35 | 35 | tree_to_stream, | |
| 36 | 36 | ) | |
| 37 | - from git.util import IndexFileSHA1Writer, finalize_process | ||
| 37 | + from git.util import IndexFileSHA1Writer, finalize_process, _validate_repo_path | ||
| 38 | 38 | ||
| 39 | 39 | from .typ import CE_EXTENDED, BaseIndexEntry, IndexEntry, CE_NAMEMASK, CE_STAGESHIFT | |
| 40 | 40 | from .util import pack, unpack | |
@@ -279,13 +279,13 @@ def write_cache( | |||
| 279 | 279 | ||
| 280 | 280 | # Body | |
| 281 | 281 | for entry in entries: | |
| 282 | + _validate_repo_path(entry.path) | ||
| 282 | 283 | beginoffset = tell() | |
| 283 | 284 | write(entry.ctime_bytes) # ctime | |
| 284 | 285 | write(entry.mtime_bytes) # mtime | |
| 285 | - path_str = str(entry.path) | ||
| 286 | + path_str = os.fspath(entry.path) | ||
| 286 | 287 | path: bytes = force_bytes(path_str, encoding=defenc) | |
| 287 | - plen = len(path) & CE_NAMEMASK # Path length | ||
| 288 | - assert plen == len(path), "Path %s too long to fit into index" % entry.path | ||
| 288 | + plen = min(len(path), CE_NAMEMASK) # Longer names use a sentinel. | ||
| 289 | 289 | flags = plen | (entry.flags & CE_NAMEMASK_INV) # Clear possible previous values. | |
| 290 | 290 | if entry.extended_flags: | |
| 291 | 291 | flags |= CE_EXTENDED | |
@@ -325,7 +325,8 @@ def read_header(stream: IO[bytes]) -> Tuple[int, int]: | |||
| 325 | 325 | unpacked = cast(Tuple[int, int], unpack(">LL", stream.read(4 * 2))) | |
| 326 | 326 | version, num_entries = unpacked | |
| 327 | 327 | ||
| 328 | - assert version in (1, 2, 3), "Unsupported git index version %i, only 1, 2, and 3 are supported" % version | ||
| 328 | + if version not in (1, 2, 3): | ||
| 329 | + raise AssertionError("Unsupported git index version %i, only 1, 2, and 3 are supported" % version) | ||
| 329 | 330 | return version, num_entries | |
| 330 | 331 | ||
| 331 | 332 | ||
@@ -382,10 +383,23 @@ def read_cache( | |||
| 382 | 383 | if flags & CE_EXTENDED: | |
| 383 | 384 | extended_flags = unpack(">H", read(2))[0] | |
| 384 | 385 | path_size = flags & CE_NAMEMASK | |
| 385 | - path = read(path_size).decode(defenc) | ||
| 386 | - | ||
| 387 | - real_size = (tell() - beginoffset + 8) & ~7 | ||
| 388 | - read((beginoffset + real_size) - tell()) | ||
| 386 | + path_bytes = bytearray(read(path_size)) | ||
| 387 | + if len(path_bytes) != path_size: | ||
| 388 | + raise ValueError("Truncated index entry path") | ||
| 389 | + terminator = read(1) | ||
| 390 | + if path_size == CE_NAMEMASK: | ||
| 391 | + while terminator and terminator != b"\0": | ||
| 392 | + path_bytes.extend(terminator) | ||
| 393 | + terminator = read(1) | ||
| 394 | + if terminator != b"\0": | ||
| 395 | + raise ValueError("Unterminated index entry path") | ||
| 396 | + path = path_bytes.decode(defenc) | ||
| 397 | + _validate_repo_path(path) | ||
| 398 | + | ||
| 399 | + real_size = (tell() - beginoffset + 7) & ~7 | ||
| 400 | + padding_size = beginoffset + real_size - tell() | ||
| 401 | + if read(padding_size) != b"\0" * padding_size: | ||
| 402 | + raise ValueError("Invalid index entry padding") | ||
| 389 | 403 | entry = IndexEntry((mode, sha, flags, path, ctime, mtime, dev, ino, uid, gid, size, extended_flags)) | |
| 390 | 404 | # entry_key would be the method to use, but we save the effort. | |
| 391 | 405 | entries[(path, entry.stage)] = entry | |
@@ -408,6 +422,18 @@ def read_cache( | |||
| 408 | 422 | # Truncate the sha in the end as we will dynamically create it anyway. | |
| 409 | 423 | extension_data = extension_data[:-20] | |
| 410 | 424 | ||
| 425 | + offset = 0 | ||
| 426 | + while offset < len(extension_data): | ||
| 427 | + header = extension_data[offset : offset + 8] | ||
| 428 | + if len(header) != 8: | ||
| 429 | + raise ValueError("Truncated index extension header") | ||
| 430 | + signature, size = unpack(">4sL", header) | ||
| 431 | + if not b"A" <= signature[:1] <= b"Z": | ||
| 432 | + raise ValueError("Unsupported mandatory index extension %r" % signature) | ||
| 433 | + offset += 8 + size | ||
| 434 | + if offset > len(extension_data): | ||
| 435 | + raise ValueError("Truncated index extension %r" % signature) | ||
| 436 | + | ||
| 411 | 437 | return (version, entries, extension_data, content_sha) | |
| 412 | 438 | ||
| 413 | 439 | ||
@@ -434,6 +460,9 @@ def write_tree_from_cache( | |||
| 434 | 460 | ||
| 435 | 461 | A tuple of a sha and a list of tree entries being a tuple of hexsha, mode, name. | |
| 436 | 462 | """ | |
| 463 | + if si == 0: | ||
| 464 | + for entry in entries[sl]: | ||
| 465 | + _validate_repo_path(entry.path) | ||
| 437 | 466 | tree_items: List["TreeCacheTup"] = [] | |
| 438 | 467 | ||
| 439 | 468 | ci = sl.start | |
@@ -481,6 +510,7 @@ def write_tree_from_cache( | |||
| 481 | 510 | ||
| 482 | 511 | ||
| 483 | 512 | def _tree_entry_to_baseindexentry(tree_entry: "TreeCacheTup", stage: int) -> BaseIndexEntry: | |
| 513 | + _validate_repo_path(tree_entry[2]) | ||
| 484 | 514 | return BaseIndexEntry((tree_entry[1], tree_entry[0], stage << CE_STAGESHIFT, tree_entry[2])) | |
| 485 | 515 | ||
| 486 | 516 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments