@@ -255,7 +255,7 @@ def _config_parser(
255255 # END handle parent_commit
256256 fp_module : Union [str , BytesIO ]
257257 if not repo .bare and parent_matches_head and repo .working_tree_dir :
258- fp_module = osp . join (repo .working_tree_dir , cls .k_modules_file )
258+ fp_module = cls . _checked_abspath (repo .working_tree_dir , cls .k_modules_file )
259259 else :
260260 assert parent_commit is not None , "need valid parent_commit in bare repositories"
261261 try :
@@ -320,9 +320,9 @@ def _validated_name(cls, name: str) -> str:
320320 def _module_abspath (cls , parent_repo : "Repo" , path : PathLike , name : str ) -> PathLike :
321321 name = cls ._validated_name (name )
322322 if cls ._need_gitfile_submodules (parent_repo .git ):
323- return osp . join (parent_repo .git_dir , "modules" , name )
323+ return cls . _checked_abspath (parent_repo .git_dir , osp . join ( "modules" , name ) )
324324 if parent_repo .working_tree_dir :
325- return osp . join (parent_repo .working_tree_dir , path )
325+ return cls . _checked_abspath (parent_repo .working_tree_dir , cls . _to_relative_path ( parent_repo , path ) )
326326 raise NotADirectoryError ()
327327
328328 @classmethod
@@ -361,8 +361,13 @@ def _clone_repo(
361361 :param kwargs:
362362 Additional arguments given to :manpage:`git-clone(1)`.
363363 """
364+ path = cls ._to_relative_path (repo , path )
365+ if repo .working_tree_dir is None :
366+ raise NotADirectoryError ("Submodules require a working tree" )
367+ module_checkout_path = cls ._checked_abspath (repo .working_tree_dir , path )
364368 module_abspath = cls ._module_abspath (repo , path , name )
365- module_checkout_path = module_abspath
369+ cls ._checked_abspath (module_checkout_path , ".git" )
370+ cls ._checked_abspath (module_abspath , "config" )
366371 if cls ._need_gitfile_submodules (repo .git ):
367372 if not allow_unsafe_options :
368373 Git .check_unsafe_options (Git ._option_candidates ([], kwargs ), repo .unsafe_git_clone_options )
@@ -377,7 +382,6 @@ def _clone_repo(
377382 module_abspath_dir = osp .dirname (module_abspath )
378383 if not osp .isdir (module_abspath_dir ):
379384 os .makedirs (module_abspath_dir )
380- module_checkout_path = osp .join (repo .working_tree_dir , path ) # type: ignore[arg-type]
381385
382386 if url .startswith ("../" ):
383387 remote_name = cast ("RemoteReference" , repo .active_branch .tracking_branch ()).remote_name
@@ -423,16 +427,23 @@ def abspath(self) -> PathLike:
423427
424428 def _checkout_abspath (self , relative_path : PathLike , allow_final_symlink : bool = False ) -> PathLike :
425429 """Check a checkout path already normalized by :meth:`_to_relative_path`."""
426- path = self .repo .working_tree_dir
427- if path is None :
430+ return self ._checked_abspath (self .repo .working_tree_dir , relative_path , allow_final_symlink )
431+
432+ @classmethod
433+ def _checked_abspath (
434+ cls , root : Union [PathLike , None ], relative_path : PathLike , allow_final_symlink : bool = False
435+ ) -> str :
436+ """Reject symlinks below a trusted root before accessing submodule paths."""
437+ if root is None :
428438 raise NotADirectoryError ("Submodules require a working tree" )
429- components = os .fspath (relative_path ).split ("/" )
439+ path = os .fspath (root )
440+ components = to_native_path_linux (relative_path ).split ("/" )
430441 for index , component in enumerate (components ):
431- path = join_path_native (path , component )
442+ path = os . fspath ( join_path_native (path , component ) )
432443 if allow_final_symlink and index == len (components ) - 1 :
433444 break
434445 if osp .islink (path ):
435- raise ValueError ("Submodule checkout path %r contains a symbolic link" % relative_path )
446+ raise ValueError ("Submodule path %r contains a symbolic link" % relative_path )
436447 return path
437448
438449 @classmethod
@@ -458,14 +469,15 @@ def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_ab
458469 :param module_abspath:
459470 Absolute path to the bare repository.
460471 """
461- git_file = osp .join (working_tree_dir , ".git" )
472+ git_file = cls ._checked_abspath (working_tree_dir , ".git" )
473+ module_config = cls ._checked_abspath (module_abspath , "config" )
462474 rela_path = osp .relpath (module_abspath , start = working_tree_dir )
463475 if sys .platform == "win32" and osp .isfile (git_file ):
464476 os .remove (git_file )
465477 with open (git_file , "wb" ) as fp :
466478 fp .write (("gitdir: %s" % rela_path ).encode (defenc ))
467479
468- with GitConfigParser (osp . join ( module_abspath , "config" ) , read_only = False , merge_includes = False ) as writer :
480+ with GitConfigParser (module_config , read_only = False , merge_includes = False ) as writer :
469481 writer .set_value (
470482 "core" ,
471483 "worktree" ,
@@ -576,6 +588,9 @@ def add(
576588 name ,
577589 url = "invalid-temporary" ,
578590 )
591+ cls ._checked_abspath (repo .working_tree_dir , cls .k_modules_file )
592+ sm ._checkout_abspath (path )
593+ cls ._module_abspath (repo , path , name )
579594 if sm .exists ():
580595 # Reretrieve submodule from tree.
581596 try :
@@ -1067,6 +1082,17 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
10671082 return self
10681083 # END handle no change
10691084
1085+ if configuration :
1086+ self ._checked_abspath (self .repo .working_tree_dir , self .k_modules_file )
1087+ # Validate the source and both metadata destinations before removing anything.
1088+ cur_path = self .abspath
1089+ module_abspath = self ._module_abspath (self .repo , self .path , self .name )
1090+ try :
1091+ self .module ().close ()
1092+ except InvalidGitRepositoryError :
1093+ pass
1094+ if self .path == self .name :
1095+ self ._module_abspath (self .repo , module_checkout_path , os .fspath (module_checkout_path ))
10701096 module_checkout_abspath = self ._checkout_abspath (module_checkout_path , allow_final_symlink = True )
10711097 if osp .isfile (module_checkout_abspath ):
10721098 raise ValueError ("Cannot move repository onto a file: %s" % module_checkout_abspath )
@@ -1099,14 +1125,12 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
10991125 # END handle module
11001126
11011127 # Move the module into place if possible.
1102- cur_path = self .abspath
11031128 renamed_module = False
11041129 if module and osp .exists (cur_path ):
11051130 os .renames (cur_path , module_checkout_abspath )
11061131 renamed_module = True
11071132
11081133 if osp .isfile (osp .join (module_checkout_abspath , ".git" )):
1109- module_abspath = self ._module_abspath (self .repo , self .path , self .name )
11101134 self ._write_git_file_and_module_config (module_checkout_abspath , module_abspath )
11111135 # END handle git file rewrite
11121136 # END move physical module
@@ -1201,6 +1225,8 @@ def remove(
12011225 # END handle parameters
12021226
12031227 self ._validated_name (self .name )
1228+ if configuration :
1229+ self ._checked_abspath (self .repo .working_tree_dir , self .k_modules_file )
12041230 # Recursively remove children of this submodule.
12051231 nc = 0
12061232 for csm in self .children ():
@@ -1460,6 +1486,9 @@ def rename(self, new_name: str) -> "Submodule":
14601486
14611487 self ._validated_name (self .name )
14621488 self ._validated_name (new_name )
1489+ destination_module_abspath = self ._module_abspath (self .repo , self .path , new_name )
1490+ mod = self .module ()
1491+ self ._checked_abspath (self .repo .working_tree_dir , self .k_modules_file )
14631492
14641493 # .git/config
14651494 with self .repo .config_writer () as pw :
@@ -1476,9 +1505,7 @@ def rename(self, new_name: str) -> "Submodule":
14761505 self ._name = new_name
14771506
14781507 # .git/modules
1479- mod = self .module ()
14801508 if mod .has_separate_working_tree ():
1481- destination_module_abspath = self ._module_abspath (self .repo , self .path , new_name )
14821509 source_dir = mod .git_dir
14831510 # Let's be sure the submodule name is not so obviously tied to a directory.
14841511 if str (destination_module_abspath ).startswith (str (mod .git_dir )):
@@ -1510,9 +1537,20 @@ def module(self) -> "Repo":
15101537 """
15111538 self ._validated_name (self .name )
15121539 module_checkout_abspath = self .abspath
1540+ module_abspath = self ._module_abspath (self .repo , self .path , self .name )
1541+ self ._checked_abspath (module_abspath , "config" )
1542+ self ._checked_abspath (module_checkout_abspath , ".git" )
15131543 try :
15141544 repo = git .Repo (module_checkout_abspath )
15151545 if repo != self .repo :
1546+ # The gitfile can name a different repository than .git/modules/<name>.
1547+ # Validate its actual path too, including old-style embedded repositories.
1548+ try :
1549+ root = osp .commonpath ([self .repo .git_dir , repo .git_dir ])
1550+ except ValueError : # Separate Windows drives have no common path.
1551+ root = osp .splitdrive (repo .git_dir )[0 ] + osp .sep
1552+ self ._checked_abspath (root , osp .relpath (repo .git_dir , root ))
1553+ self ._checked_abspath (repo .git_dir , "config" )
15161554 return repo
15171555 # END handle repo uninitialized
15181556 except (InvalidGitRepositoryError , NoSuchPathError ) as e :
0 commit comments