Skip to content

chore(deps): update dependency gitpython to v3.1.62 [security] - #31

Open
dependency-automation-app[bot] wants to merge 1 commit into
masterfrom
renovate/pypi-gitpython-vulnerability
Open

dependency-automation-app[bot] wants to merge 1 commit into
masterfrom
renovate/pypi-gitpython-vulnerability

Conversation

@dependency-automation-app

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
gitpython 3.1.60 -> 3.1.62 age confidence

GitHub Vulnerability Alerts

GHSA-59cr-6r3x-644w

Affected: GitPython 3.1.61 (latest release) and main — git/objects/submodule/base.py. git diff 3.1.61 origin/main -- git/objects/submodule/ is empty, so both are identical here.


The gap

The fix for GHSA-hmq2-w58f-27jc added Submodule._validated_name() and wired it into update() and five siblings, closing the .gitmodules name → .git/modules/<name> traversal. The other attacker-controlled .gitmodules field, path, is read raw:

# git/objects/submodule/base.py:172-177
def _set_cache_(self, attr):
    if attr in ("path", "_url", "_branch_path"):
        reader = self.config_reader()
        self.path = reader.get("path")          # raw .gitmodules value

and GitPython's own containment guard is applied in only two of the places that consume it:

400: def _to_relative_path(cls, parent_repo, path)      # the guard (abspath + commonpath containment)
542:     path = cls._to_relative_path(repo, path)        # add()   — guarded
1041:    module_checkout_path = self._to_relative_path(self.repo, module_path)   # move() — guarded

update() validates only the name and then uses the path-derived absolute location directly:

788:  self._validated_name(self.name)                    # NAME only
801:  checkout_module_abspath = self.abspath             # derived from self.path — unguarded
821:  os.makedirs(checkout_module_abspath, exist_ok=True)

So path = ../../../tmp/escaped in an attacker-authored .gitmodules selects the directory that gets created and, on the clone path, populated from the submodule URL. The same absolute location is what force_remove hands to shutil.rmtree.

The asymmetry is the argument: this is not a missing concept — the project wrote _to_relative_path() precisely for this, and add()/move() use it. update() does not.

Honest limits (please read before rating)

  • The most common flow is not affected. Repo.clone_from(...) → repo.submodules → sm.update(init=True) re-derives path from a canonical tree lookup, and real git refuses to check out a tree containing a .. component, so an evil .gitmodules never lands in the working tree in the first place. A reachable trigger therefore requires the victim's code to name a non-HEAD commit (a historical-commit API such as submodule_update(previous_commit=...)).
  • The researcher did not build that end-to-end trigger. The researcher only verified first-hand the code above: the guard's two call sites, the name-only validation in update(), and the unguarded abspath → os.makedirs() flow at 3.1.61 == main.

Suggested fix

Apply the guard the project already has, wherever the path is consumed:

# in update(), before deriving abspath (and in any other consumer of self.path):
checkout_rel = self._to_relative_path(self.repo, self.path)   # raises if it escapes the working tree

Better still, validate at the boundary: reject a .gitmodules entry whose path is absolute or contains a .. component when the section is first read in _set_cache_()/iter_items(), so no consumer can be added later without the check. A regression test with path = ../escaped alongside the existing name test would pin both fields.

Prior art checked

GHSA-hmq2-w58f-27jc (this is a residual of its fix, in the sibling field, not a re-report) plus the repository's 30 published advisories — none mentions the path field or _to_relative_path. Searched issues and PRs for _to_relative_path, gitmodules path and submodule traversal: no report of this.

Credit

kta1kri.


Appendix — EVIDENCE_gitpython_path_unguarded_20260901.txt (inlined; advisories accept no attachments)

=== EVIDENCE: GitPython — the .gitmodules 'path' field reaches os.makedirs()/clone unguarded ===
Mon Aug 31 18:45:22 UTC 2026

--- artifact: tag 3.1.61 (latest release); git diff 3.1.61 origin/main -- git/objects/submodule/ is empty ---

--- the containment guard GitPython owns, and its only two call sites ---
33:    _to_relative_path,
400:    def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
407:            path = _to_relative_path(parent_repo.working_tree_dir, path)
542:        path = cls._to_relative_path(repo, path)
1041:        module_checkout_path = self._to_relative_path(self.repo, module_path)

--- the parent fix (_validated_name) call sites: it validates the NAME ---
309:    def _validated_name(cls, name: str) -> str:
321:        name = cls._validated_name(name)
541:        cls._validated_name(name)
788:            self._validated_name(self.name)
1040:        self._validated_name(self.name)
1181:        self._validated_name(self.name)
1439:        self._validated_name(self.name)
1440:        self._validated_name(new_name)
1489:        self._validated_name(self.name)

--- update(): name validated, path not; abspath -> os.makedirs ---

        try:
            self._validated_name(self.name)

            # ENSURE REPO IS PRESENT AND UP-TO-DATE
                # END early abort if init is not allowed

                checkout_module_abspath = self.abspath
                module_abspath = self._module_abspath(self.repo, self.path, self.name)

                # ``git submodule deinit`` leaves the repository in
                # ``.git/modules`` and empties the checkout. Reconnect that retained
                # repository instead of trying to clone over it.
                if not dry_run and osp.isdir(module_abspath):
                    try:
                        git.Repo(module_abspath)
                    except InvalidGitRepositoryError:
                        pass
                    else:
                        if osp.lexists(checkout_module_abspath) and (
                            osp.islink(checkout_module_abspath)
                            or not osp.isdir(checkout_module_abspath)
                            or os.listdir(checkout_module_abspath)
                        ):
                            raise OSError(
                                "Module directory at %r does already exist and is non-empty" % checkout_module_abspath
                            )
                        os.makedirs(checkout_module_abspath, exist_ok=True)
                        self._write_git_file_and_module_config(checkout_module_abspath, module_abspath)
                        mrepo = git.Repo(checkout_module_abspath)

--- where self.path comes from (raw .gitmodules value) ---
    def _set_cache_(self, attr: str) -> None:
        if attr in ("path", "_url", "_branch_path"):
            reader: SectionConstraint = self.config_reader()
            # Default submodule values.
            try:
                self.path = reader.get("path")
            except cp.NoSectionError as e:

Configuration

📅 Schedule: Branch creation - "" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR has been generated by Renovate Bot.

@simonirwin-r7

simonirwin-r7 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant