fix: Reject submodule move destinations through intermediate symlinks · gitpython-developers/GitPython@24b6f95 · GitHub
Skip to content

Commit 24b6f95

Browse files
Byroncodex
andcommitted
fix: Reject submodule move destinations through intermediate symlinks
Also, sloppy review of the tests which are assumpted to not make things worse. <!-- agent --> Submodule.move() checked lexical containment but did not validate intermediate destination components before filesystem and repository updates. GHSA-gq48-pqfc-9p58 identifies the resulting checkout-path boundary violation. The new regression failed before the fix because move() returned successfully. Share the existing abspath component walk with move() and validate the normalized destination before any mutation, including configuration-only and module-only calls. Preserve the no-op early return and existing final-component symlink handling; abspath still rejects every symlink component. This addresses pre-existing links, not concurrent directory replacement races. The Git reference checkout at 1630431f326e15fcde608827b5ff38422528eb59 uses has_symlink_leading_path() in builtin/mv.c and tests rejection without index changes in t/t7001-mv.sh. The fix follows that intermediate-component rule while retaining GitPython leaf-link compatibility. Validation: the 30 new parameterized cases pass, covering relative and absolute destinations and link targets, internal and dangling links, all move flag combinations, unchanged repository state after rejection, ordinary and no-op moves, and leaf-link compatibility. The complete test/test_submodule.py suite passes: 75 passed, 3 skipped, 1 xfailed. Test-process commit.gpgsign=false avoids sandbox GPG failures. Ruff lint and format checks, mypy (45 source files), and git diff --check pass. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
1 parent b62e91b commit 24b6f95

3 files changed

Lines changed: 153 additions & 5 deletions

File tree

doc/source/changes.rst

Lines changed: 13 additions & 0 deletions

git/objects/submodule/base.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -419,11 +419,20 @@ def abspath(self) -> PathLike:
419419
root = self.repo.working_tree_dir
420420
if root is None:
421421
return super().abspath
422-
path = root
423-
for component in os.fspath(self._to_relative_path(self.repo, self.path)).split("/"):
422+
return self._checkout_abspath(self._to_relative_path(self.repo, self.path))
423+
424+
def _checkout_abspath(self, relative_path: PathLike, allow_final_symlink: bool = False) -> PathLike:
425+
"""Check a checkout path already normalized by :meth:`_to_relative_path`."""
426+
path = self.repo.working_tree_dir
427+
if path is None:
428+
raise NotADirectoryError("Submodules require a working tree")
429+
components = os.fspath(relative_path).split("/")
430+
for index, component in enumerate(components):
424431
path = join_path_native(path, component)
432+
if allow_final_symlink and index == len(components) - 1:
433+
break
425434
if osp.islink(path):
426-
raise ValueError("Submodule checkout path %r contains a symbolic link" % self.path)
435+
raise ValueError("Submodule checkout path %r contains a symbolic link" % relative_path)
427436
return path
428437

429438
@classmethod
@@ -1039,7 +1048,8 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
10391048
self
10401049
10411050
:raise ValueError:
1042-
If the module path existed and was not empty, or was a file.
1051+
If the module path existed and was not empty, was a file, or had a
1052+
symbolic link in an intermediate component.
10431053
10441054
:note:
10451055
Currently the method is not atomic, and it could leave the repository in an
@@ -1057,7 +1067,7 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
10571067
return self
10581068
# END handle no change
10591069

1060-
module_checkout_abspath = join_path_native(str(self.repo.working_tree_dir), module_checkout_path)
1070+
module_checkout_abspath = self._checkout_abspath(module_checkout_path, allow_final_symlink=True)
10611071
if osp.isfile(module_checkout_abspath):
10621072
raise ValueError("Cannot move repository onto a file: %s" % module_checkout_abspath)
10631073
# END handle target files

test/test_submodule.py

Lines changed: 125 additions & 0 deletions

0 commit comments

Comments
 (0)