Skip to content

Commit f9e74ab

Browse files
authored
Merge pull request #2278 from Keerthana-64/gitmodules-symlink-entries
fix(util): reject symbolic links that alias `.gitmodules`
2 parents 1af7ce6 + 9f56080 commit f9e74ab

7 files changed

Lines changed: 304 additions & 52 deletions

File tree

‎git/index/base.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,8 @@ def _preprocess_add_items(
728728
else:
729729
raise TypeError("Invalid Type: %r" % item)
730730
# END for each item
731+
# Source paths must be safe to read, but their recorded names may be rewritten.
732+
# Apply mode-dependent restrictions to the final entries in add().
731733
for entry in entries:
732734
_validate_repo_path(entry.path)
733735
return paths, entries
@@ -1026,7 +1028,7 @@ def handle_null_entries(self: "IndexFile") -> None:
10261028
# FINALIZE
10271029
# Add the new entries to this instance.
10281030
for entry in entries_added:
1029-
_validate_repo_path(entry.path)
1031+
_validate_repo_path(entry.path, entry.mode)
10301032
for entry in entries_added:
10311033
self.entries[(entry.path, 0)] = IndexEntry.from_base(entry)
10321034

‎git/index/fun.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ def write_cache(
279279

280280
# Body
281281
for entry in entries:
282-
_validate_repo_path(entry.path)
282+
_validate_repo_path(entry.path, entry.mode)
283283
beginoffset = tell()
284284
write(entry.ctime_bytes) # ctime
285285
write(entry.mtime_bytes) # mtime
@@ -394,7 +394,7 @@ def read_cache(
394394
if terminator != b"\0":
395395
raise ValueError("Unterminated index entry path")
396396
path = path_bytes.decode(defenc)
397-
_validate_repo_path(path)
397+
_validate_repo_path(path, mode)
398398

399399
real_size = (tell() - beginoffset + 7) & ~7
400400
padding_size = beginoffset + real_size - tell()
@@ -462,7 +462,7 @@ def write_tree_from_cache(
462462
"""
463463
if si == 0:
464464
for entry in entries[sl]:
465-
_validate_repo_path(entry.path)
465+
_validate_repo_path(entry.path, entry.mode)
466466
tree_items: List["TreeCacheTup"] = []
467467

468468
ci = sl.start
@@ -510,7 +510,7 @@ def write_tree_from_cache(
510510

511511

512512
def _tree_entry_to_baseindexentry(tree_entry: "TreeCacheTup", stage: int) -> BaseIndexEntry:
513-
_validate_repo_path(tree_entry[2])
513+
_validate_repo_path(tree_entry[2], tree_entry[1])
514514
return BaseIndexEntry((tree_entry[1], tree_entry[0], stage << CE_STAGESHIFT, tree_entry[2]))
515515

516516

‎git/objects/fun.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@
3939
# ---------------------------------------------------
4040

4141

42-
def _validate_tree_entry_name(name: str) -> None:
42+
def _validate_tree_entry_name(name: str, mode: Union[int, None] = None) -> None:
4343
if "/" in name:
4444
raise ValueError("Tree entry names must not contain '/' characters")
4545
# A tree name is a component, not a rooted path; a colon cannot select a drive.
46-
_validate_repo_path("tree/" + name)
46+
_validate_repo_path("tree/" + name, mode)
4747

4848

4949
def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer"], Union[int, None]]) -> None:
@@ -82,7 +82,7 @@ def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer
8282
name_bytes = name.encode(defenc)
8383
else:
8484
name_bytes = name # type: ignore[unreachable] # check runtime types - is always str?
85-
_validate_tree_entry_name(safe_decode(name_bytes))
85+
_validate_tree_entry_name(safe_decode(name_bytes), mode)
8686
write(b"".join((mode_str, b" ", name_bytes, b"\0", binsha)))
8787
# END for each item
8888

@@ -112,7 +112,7 @@ def tree_entries_from_data(data: bytes) -> List[EntryTup]:
112112
if name_end < 0 or name_end + 21 > len(data):
113113
raise ValueError("Truncated tree entry")
114114
name = safe_decode(bytes(data[mode_end + 1 : name_end]))
115-
_validate_tree_entry_name(name)
115+
_validate_tree_entry_name(name, mode)
116116
offset = name_end + 21
117117
out.append((bytes(data[name_end + 1 : offset]), mode, name))
118118
return out

‎git/objects/tree.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> "TreeMod
110110
:return:
111111
self
112112
"""
113-
_validate_tree_entry_name(name)
113+
_validate_tree_entry_name(name, mode)
114114
if (mode >> 12) not in Tree._map_id_to_type:
115115
raise ValueError("Invalid object type according to mode %o" % mode)
116116

‎git/util.py‎

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,63 +29,62 @@
2929
if sys.platform == "win32":
3030
__all__.append("to_native_path_windows")
3131

32-
from abc import abstractmethod
3332
import contextlib
34-
from functools import wraps
3533
import getpass
3634
import logging
3735
import ntpath
3836
import os
3937
import os.path as osp
40-
from pathlib import Path
4138
import platform
4239
import re
4340
import shutil
4441
import stat
4542
import subprocess
4643
import time
47-
from urllib.parse import urlsplit, urlunsplit
4844
import warnings
49-
50-
# NOTE: Unused imports can be improved now that CI testing has fully resumed. Some of
51-
# these be used indirectly through other GitPython modules, which avoids having to write
52-
# gitdb all the time in their imports. They are not in __all__, at least currently,
53-
# because they could be removed or changed at any time, and so should not be considered
54-
# conceptually public to code outside GitPython. Linters of course do not like it.
55-
from gitdb.util import (
56-
LazyMixin, # noqa: F401
57-
LockedFD, # noqa: F401
58-
bin_to_hex, # noqa: F401
59-
file_contents_ro, # noqa: F401
60-
file_contents_ro_filepath, # noqa: F401
61-
hex_to_bin, # noqa: F401
62-
make_sha,
63-
to_bin_sha, # noqa: F401
64-
to_hex_sha, # noqa: F401
65-
)
45+
from abc import abstractmethod
46+
from functools import wraps
47+
from pathlib import Path
6648

6749
# typing ---------------------------------------------------------
68-
6950
from typing import (
51+
IO,
52+
TYPE_CHECKING,
7053
Any,
7154
AnyStr,
7255
Callable,
7356
Dict,
7457
Generator,
75-
IO,
7658
Iterator,
7759
List,
7860
Optional,
7961
Pattern,
8062
Sequence,
8163
Tuple,
82-
TYPE_CHECKING,
8364
Type,
8465
TypeVar,
8566
Union,
8667
cast,
8768
overload,
8869
)
70+
from urllib.parse import urlsplit, urlunsplit
71+
72+
# NOTE: Unused imports can be improved now that CI testing has fully resumed. Some of
73+
# these be used indirectly through other GitPython modules, which avoids having to write
74+
# gitdb all the time in their imports. They are not in __all__, at least currently,
75+
# because they could be removed or changed at any time, and so should not be considered
76+
# conceptually public to code outside GitPython. Linters of course do not like it.
77+
from gitdb.util import (
78+
LazyMixin, # noqa: F401
79+
LockedFD, # noqa: F401
80+
bin_to_hex, # noqa: F401
81+
file_contents_ro, # noqa: F401
82+
file_contents_ro_filepath, # noqa: F401
83+
hex_to_bin, # noqa: F401
84+
make_sha,
85+
to_bin_sha, # noqa: F401
86+
to_hex_sha, # noqa: F401
87+
)
8988

9089
if TYPE_CHECKING:
9190
from git.cmd import Git
@@ -94,9 +93,9 @@
9493
from git.repo.base import Repo
9594

9695
from git.types import (
96+
HSH_TD,
9797
Files_TD,
9898
Has_id_attribute,
99-
HSH_TD,
10099
Literal,
101100
PathLike,
102101
Protocol,
@@ -385,12 +384,27 @@ def _to_relative_path(root: PathLike, path: PathLike) -> str:
385384
"", "", "\u200c\u200d\u200e\u200f\u202a\u202b\u202c\u202d\u202e\u206a\u206b\u206c\u206d\u206e\u206f\ufeff"
386385
)
387386

387+
# Match Git's is_ntfs_dotgitmodules in path.c on a lowercased, trimmed name.
388+
# Besides gitmod~1..4, fallback aliases have exactly eight ASCII characters:
389+
# a shrinking prefix of "gi7eba", "~", and digits with no leading zero.
390+
# Explicit digit counts avoid accepting shorter/longer names or Unicode digits.
391+
_NTFS_DOTGITMODULES_SHORT_NAME = re.compile(
392+
r"(?:gitmod~[1-4]|gi7eba~[1-9]|gi7eb~[1-9][0-9]|gi7e~[1-9][0-9]{2}|"
393+
r"gi7~[1-9][0-9]{3}|gi~[1-9][0-9]{4}|g~[1-9][0-9]{5}|~[1-9][0-9]{6})"
394+
)
395+
388396

389-
def _validate_repo_path(path: PathLike) -> None:
397+
def _validate_repo_path(path: PathLike, mode: Union[int, None] = None) -> None:
390398
"""Reject unsafe tree/index paths without normalizing away their components.
391399
392400
Protect Git metadata aliases on NTFS and HFS even when writing on another
393401
platform. Other POSIX filename characters, including newlines, remain valid.
402+
403+
:param mode:
404+
Mode of the index or tree entry the path belongs to, where one is known.
405+
Git refuses a symbolic link that aliases ``.gitmodules``, since the
406+
submodule configuration would then be read through the link, so that
407+
name is only rejected once the mode says the entry is a link.
394408
"""
395409
name = os.fspath(path)
396410
if not name or "\0" in name or ntpath.splitdrive(name)[0] or name.startswith("/"):
@@ -406,6 +420,13 @@ def _validate_repo_path(path: PathLike) -> None:
406420
hfs_name = part.translate(_HFS_IGNORABLES).lower()
407421
if ntfs_name in (".git", "git~1") or hfs_name == ".git":
408422
raise ValueError("Repository path aliases Git metadata: %r" % name)
423+
if mode is not None and stat.S_ISLNK(mode):
424+
if (
425+
ntfs_name == ".gitmodules"
426+
or hfs_name == ".gitmodules"
427+
or _NTFS_DOTGITMODULES_SHORT_NAME.fullmatch(ntfs_name) is not None
428+
):
429+
raise ValueError("Symbolic link aliases the submodule configuration: %r" % name)
409430

410431

411432
def assure_directory_exists(path: PathLike, is_file: bool = False) -> bool:

0 commit comments

Comments
 (0)