diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 97fc9e111..93c7cbd79 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -6,6 +6,7 @@ import gc from io import BytesIO import logging +import ntpath import os import os.path as osp import stat @@ -302,10 +303,21 @@ def _config_parser_constrained(self, read_only: bool) -> SectionConstraint: parser.set_submodule(self) return SectionConstraint(parser, sm_section(self.name)) + @classmethod + def _validated_name(cls, name: str) -> str: + if ( + not name + or name.startswith(("/", "\\")) + or ntpath.splitdrive(name)[0] + or ".." in name.replace("\\", "/").split("/") + ): + raise ValueError("Invalid submodule name %r" % name) + return name + @classmethod def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> PathLike: if cls._need_gitfile_submodules(parent_repo.git): - return osp.join(parent_repo.git_dir, "modules", name) + return osp.join(parent_repo.git_dir, "modules", cls._validated_name(name)) if parent_repo.working_tree_dir: return osp.join(parent_repo.working_tree_dir, path) raise NotADirectoryError() @@ -523,6 +535,7 @@ def add( raise InvalidGitRepositoryError("Cannot add submodules to bare repositories") # END handle bare repos + cls._validated_name(name) path = cls._to_relative_path(repo, path) # Ensure we never put backslashes into the URL, as might happen on Windows. @@ -726,6 +739,7 @@ def update( return self # END pass in bare mode + self._validated_name(self.name) if progress is None: progress = UpdateProgress() # END handle progress @@ -1020,6 +1034,7 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = raise ValueError("You must specify to move at least the module or the configuration of the submodule") # END handle input + self._validated_name(self.name) module_checkout_path = self._to_relative_path(self.repo, module_path) # VERIFY DESTINATION @@ -1160,6 +1175,7 @@ def remove( raise ValueError("Need to specify to delete at least the module, or the configuration") # END handle parameters + self._validated_name(self.name) # Recursively remove children of this submodule. nc = 0 for csm in self.children(): @@ -1416,6 +1432,9 @@ def rename(self, new_name: str) -> "Submodule": if self.name == new_name: return self + self._validated_name(self.name) + self._validated_name(new_name) + # .git/config with self.repo.config_writer() as pw: # As we ourselves didn't write anything about submodules into the parent @@ -1463,6 +1482,7 @@ def module(self) -> "Repo": If a repository was not available. This could also mean that it was not yet initialized. """ + self._validated_name(self.name) module_checkout_abspath = self.abspath try: repo = git.Repo(module_checkout_abspath) diff --git a/test/test_submodule.py b/test/test_submodule.py index 23e2c0e63..b7d1c2c0d 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -925,6 +925,52 @@ def test_update_submodule_with_relative_path(self, rwdir): cloned_repo.submodule_update(init=True, recursive=True) + @with_rw_directory + @_patch_git_config("protocol.file.allow", "always") + def test_update_rejects_parent_component_in_name(self, rwdir): + source = git.Repo.init(osp.join(rwdir, "source")) + source.git.commit(m="initial commit", allow_empty=True) + + parent = git.Repo.init(osp.join(rwdir, "parent")) + parent.git.submodule("add", source.working_tree_dir, "module") + parent.index.commit("add submodule") + modules_file = Path(parent.working_tree_dir) / ".gitmodules" + modules_file.write_text( + modules_file.read_text().replace('submodule "module"', 'submodule "../../../escaped/module"') + ) + parent.index.add([".gitmodules"]) + parent.index.commit("change submodule name") + + clone = git.Repo.clone_from(parent.working_tree_dir, osp.join(rwdir, "clone")) + with pytest.raises(ValueError, match="submodule name"): + clone.submodules[0].update(init=True) + assert not osp.exists(osp.join(rwdir, "escaped")) + + Path(rwdir, "escaped").mkdir() + git.Repo.clone_from( + source.working_tree_dir, + osp.join(clone.working_tree_dir, "module"), + separate_git_dir=osp.join(rwdir, "escaped", "module"), + ) + with pytest.raises(ValueError, match="submodule name"): + clone.submodules[0].update(init=True) + + invalid_names = ( + "", + "..", + "../module", + R"..\module", + "nested/../module", + R"nested\..\module", + "/module", + R"\module", + "C:module", + R"C:\module", + ) + for name in invalid_names: + with pytest.raises(ValueError, match="submodule name"): + Submodule._module_abspath(clone, "module", name) + @with_rw_directory @_patch_git_config("protocol.file.allow", "always") def test_list_only_valid_submodules(self, rwdir):