Highghsa:GHSA-23mf-xhv8-69c2

    GitPython: Exposure of Sensitive Information to an Unauthorized Actor (ghsa:GHSA-23mf-xhv8-69c2)

    Published Sources: Hersteller

    Share

    What is affected, what to do

    Update GitPython to version 3.2.0.

    Not sure whether and how you are affected? Ask a question about this vulnerability

    Metrics

    —

    XIEM Effective CVSS

    No value is available for this vulnerability.

    —

    CVSS v3

    No value is available for this vulnerability.

    —

    EPSS score

    No value is available for this vulnerability.

    Show all metrics
    Severity
    High
    Priority (0–325)

    The priority score is a consolidated metric that condenses several individual signals into a single, action-guiding ranking: technical severity (CVSS), exploitation likelihood (EPSS), known-exploited status (KEV) and the context of the affected asset. The aim is to answer the only truly important question in vulnerability management: what first?

    Read in glossary
    30.00 Methodology
    Exploit maturity

    An exploit is code or a technique that specifically uses a concrete vulnerability to force unintended behaviour — such as executing code or escalating privileges. It is the tool that turns a theoretical flaw into a practical attack.

    Read in glossary
    no public PoC known
    KEV status

    The KEV catalogue is a list maintained by the US authority CISA of vulnerabilities for which active exploitation in the wild has been confirmed. It answers not the question „how severe?“ but „is it already being attacked?“ — thereby sharply prioritising patching.

    Read in glossary
    —
    Published
    CWE

    CWE is a catalogue of typical weakness types in software and hardware — such as flawed input validation, unsafe memory handling or poor authentication. Where a CVE names a concrete vulnerability in a concrete product, a CWE describes the underlying class of defect.

    Read in glossary
    CWE-200, CWE-73, CWE-88

    Classification under NIS2 and CRA

    Rule-based classification from exploitation status and severity. It does not replace a case-by-case assessment.

    NIS2

    NIS2 is EU Directive (EU) 2022/2555 to raise the level of cybersecurity across the Union. It substantially broadens obligations compared with the first NIS directive: more sectors, mandatory risk management, staged reporting duties and personal accountability of senior management.

    Read in glossary
    High technical severity: assess and remediate the vulnerability in vulnerability handling under Art. 21(2)(e) NIS2 and document the decision.
    CRA

    The CRA is EU Regulation (EU) 2024/2847 setting horizontal cybersecurity requirements for „products with digital elements“ — from industrial controllers and software to connected consumer devices. For the first time manufacturers must ensure security across the entire product lifecycle, handle vulnerabilities and provide security updates.

    Read in glossary
    From 11.12.2027, Annex I Part II CRA requires manufacturers of affected products with digital elements to handle vulnerabilities without delay and provide security updates.
    ISO/IEC 27001

    ISO/IEC 27001 is the internationally leading standard for an information security management system (ISMS). It describes a risk-based, process-oriented framework to systematically plan, implement, review and improve information security.

    Read in glossary
    Control A.8.8, management of technical vulnerabilities: assess relevance to your systems, plan remediation and keep evidence.

    Description

    A note on the referenced files. This report was submitted through GitHub's private vulnerability reporting API, which carries no attachments. Every transcript this report leans on is reproduced inline above, verbatim. The file names it mentions (poc.sh, poc.log, per-case transcripts, evidence.md, negative-controls.md, blob-sweep tables) are the local evidence bundle those inline excerpts were extracted from, with a checksum manifest. I am happy to send the bundle by any channel you prefer, or to open a private fork — just say which. The PoC scripts are self-contained and reproduce the result from a clean environment.


    Affected: <= 3.1.62 — all published releases; also main @ 4905b4af Verified at runtime: GitPython 3.1.62 with git 2.55.0 Date: 2026-09-10 Reporter: MAXI8594


    Threat model

    Attackeranyone who can get the application to clone a URL they control. No account, no credential, no privilege in the consuming application.
    Pre-needa repository they host, with HEAD pointing at a branch whose name is the payload. A git push to a plain git server is enough — no filesystem access to a server is required (raw/06).
    Getsthe contents of any file the application's process can read, returned in band through the exception. /etc/passwd recovered 34 of 34 lines.
    Real-world likelihoodHigh. The vulnerable consumer is six lines of ordinary code with no unusual configuration: Repo.clone_from(user_url) then repo.active_branch.checkout(). allow_unsafe_options=False is the default. Cloning user-supplied URLs is the library's central use case — CI systems, repository importers, scanners and bots.

    On severity relative to the sibling advisories. GitHub, as CNA, scored the three prior advisories in this class at 6.5 / PR:L (CVE-2026-76217, CVE-2026-78678, CVE-2026-78679). This report asks for 7.5 / PR:N, and the difference is one measured fact: in those three, the application had to pass an attacker-influenced value into the API. Here it passes nothing — the clone materialises the hostile branch on its own and repo.active_branch returns it (raw/02). If that reading is not accepted, the PR:L path via Repo.create_head() is also real and lands at 6.5, matching the siblings.

    Summary

    Head.checkout() guards its **kwargs against unsafe Git options but never inspects its own positional argument — the branch name — which reaches the git checkout argv with no -- separator. A repository whose default branch is named --pathspec-from-file=/etc/passwd makes a consumer that merely clones it and checks out the default branch return the entire contents of that file to the caller.

    This is the exact combination of two advisories the project already fixed:

    • CVE-2026-76217 (GHSA-hh9p-6wh2-4mfc) — "Arbitrary file read via --pathspec-from-file in IndexFile.remove() and Head.checkout()". Fixed in 3.1.58 by commit f2550b65, which added the guard to this exact method — but only for **kwargs.
    • CVE-2026-78679 (GHSA-3wxw-xv34-2frg) — "TagReference.create positional reference bypasses kwargs-only --file guard, enabling arbitrary file read (incomplete fix of 3af0c251)". Fixed in 3.1.59 by commit 1b0d2d9b, which applied the positional check to git/refs/tag.py, git/diff.py, git/index/base.py and git/repo/base.py — but not to git/refs/head.py:286.

    f2550b65 states in its own commit message that the audit found the positional variant and applied it to HEAD.reset — 185 lines above Head.checkout, in the same file, in the same commit:

    "The audit also found that reset's positional commit could carry the option before its argument separator. […] Check reset's positional commit as well as keyword options"

    HEAD.reset got the fix. Head.checkout did not, and 1b0d2d9b did not come back for it.

    Affected Asset

    PackageGitPython (PyPI)
    Affected versions<= 3.1.62 — all published releases
    Latest release tested3.1.62
    Also present onmain @ 4905b4af
    Vulnerable filegit/refs/head.py
    Guardline 286 — Git._option_candidates([], kwargs)
    Sinkline 293 — self.repo.git.checkout(self, **kwargs)
    CWECWE-88 (Argument Injection), CWE-73 (External Control of File Name or Path), CWE-200 (Exposure of Sensitive Information)
    CVSS 3.1AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N → 7.5 High

    The version range was established by blob hash against every tag, not by dates: the sink line self.repo.git.checkout(self, **kwargs) — with no -- separator — is present in git/refs/head.py in every tag from 0.3.1-beta2 through 3.1.62. The 3.1.62 blob is byte-identical to main: 7f563374ef9e018b3d5f95054498e8591e112ce2. Full sweep in raw/07-blob-version-sweep.txt.

    The kwargs-only guard first appears in 3.1.58, so 3.1.58–3.1.62 is where "incomplete fix" applies; earlier releases have the same sink with no guard at all.

    Vulnerability Details

    Root cause

    git/cmd.py:1045 — _option_candidates() only inspects the positionals it is handed:

    @classmethod
    def _option_candidates(cls, args: Sequence[Any] = (), kwargs: Optional[Mapping[str, Any]] = None) -> List[str]:
        """Collect possible option spellings before command-line transformation."""
        options = [
            option for option in cls._unpack_args([arg for arg in args if arg is not None]) if option.startswith("-")
        ]
    

    So _option_candidates([], kwargs) means: check the keyword arguments, never check the positional arguments this method puts on the command line. That is fine when the method has no attacker-influenced positional, or when it separates them with --.

    git/refs/head.py:283-293 — Head.checkout() has neither:

            if not allow_unsafe_options:
                Git.check_unsafe_options(
                    options=Git._option_candidates([], kwargs),
                    unsafe_options=Git.unsafe_git_pathspec_from_file_options,
                )
            kwargs["f"] = force
            if kwargs["f"] is False:
                kwargs.pop("f")
    
            self.repo.git.checkout(self, **kwargs)
    

    self is the only positional. It serialises to the branch name, lands in the git checkout argv, and is not in the list the guard inspects. _call_process builds args_list = opt_args + ext_args, so keyword-derived options precede positionals — which is why the rest of the library puts "--" first (Remote.fetch/pull/push, Repo._clone, Repo.archive, IndexFile.remove, Commit.count all do). Head.checkout does not.

    Why the branch name is attacker-controlled

    Git accepts a refname whose branch component begins with - and contains / (raw/02-refname-accepted-and-cloned.txt):

    >>> git check-ref-format 'refs/heads/--pathspec-from-file=/etc/passwd'
        exit=0  stderr=''
        => un refname que empieza con '-' y lleva '/' es VALIDO para git
    
    >>> git -C <bare> update-ref 'refs/heads/--pathspec-from-file=/etc/passwd' beea87239099
        exit=0  stderr=''
    >>> git -C <bare> symbolic-ref HEAD 'refs/heads/--pathspec-from-file=/etc/passwd'
        exit=0
        HEAD del bare = refs/heads/--pathspec-from-file=/etc/passwd
        ref en disco como arbol jerarquico: True
    
    >>> Repo.clone_from('file://<bare>', <victim>)   # la app solo clona
        ramas LOCALES creadas por el clone, sin que nadie las pida:
          refs/heads/--pathspec-from-file=/etc/passwd
        repo.active_branch.name = '--pathspec-from-file=/etc/passwd'
        coincide con el nombre hostil: True
    

    The transcript above also shows the second half: git clone materialises that branch locally on its own when the remote HEAD points at it — the consuming application never asks for it.

    A plain git server accepts the refname over receive-pack, with no --force and no special config (raw/06-push-accepts-refname.txt), so the attacker does no

    Source: GitHub Security Advisories

    Affected products

    The following versions are affected. Older releases of the same product line are also vulnerable unless stated otherwise.

    Affected products
    Affected versionsFixed in
    pipGitPython
    • <= 3.1.62
    • 3.2.0

    Weakness classes (CWE)

    • CWE-200Exposure of Sensitive Information to an Unauthorized Actor

      Class

      The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.

      Entry at MITRE CWE-200
    • CWE-73External Control of File Name or Path

      Base

      The product allows user input to control or influence paths or file names that are used in filesystem operations.

      Entry at MITRE CWE-73
    • CWE-88Improper Neutralization of Argument Delimiters in a Command ('Argument Injection')

      Base

      The product constructs a string for a command to be executed by a separate component in another control sphere, but it does not properly delimit the intended arguments, options, or switches within that command string.

      Entry at MITRE CWE-88