Strona główna / Blog / GitPython: repo staje się kodem

Blog · case study

GitPython CVE-2026-87817: złośliwe repozytorium może podszyć się pod własny „.git” i wykonać pre-commit hook. Kiedy „code from Git” staje się supply-chain execution primitive

CVE-2026-87817 dotyczy GitPythona przed 3.1.60. Problem to niewystarczająca walidacja lokalizacji git directory: złośliwe repozytorium może użyć śledzonych plików (gitdir, commondir, HEAD) do podszycia się pod strukturę Gita i umieścić malicious pre-commit hook w kontrolowanym katalogu. Gdy aplikacja wywoła index.commit(), hook uruchamia arbitrary code. Ciekawe jest to, że payload dostarczany jest jako repo data, a execution następuje dopiero podczas późniejszej operacji biblioteki.

CVE-2026-87817 (VulnCheck CVSS v4 8.7): impersonacja git directory → attacker-controlled hooks path → index.commit() uruchamia pre-commit hook w kontekście procesu.
Fix: GitPython ≥ 3.1.60; remediacja musi objąć też consuming applications, nie tylko paczkę.

Attack path: dane, które stają się kodem

attacker prepares crafted repository -> victim clones/opens repository via GitPython -> tracked files influence perceived git directory -> attacker-controlled hooks path becomes trusted -> application calls index.commit() -> pre-commit hook executes -> arbitrary code in victim process context

To nie jest klasyczne „uruchomiłeś skrypt z repo”. Użytkownik może sądzić, że jedynie pracuje na danych Git. Warunki exploitacji: attacker kontroluje repo, repo zostaje otwarte/sklonowane w podatnym workflow, aplikacja dochodzi do index.commit(), a hook zostaje odnaleziony przez zmanipulowaną strukturę. Czyli package present != automatically exploitable — reachability jest kluczowe.

Dlaczego SCA myli się w obie strony

False positive operacyjny: GitPython jest zainstalowane, ale aplikacja tylko czyta historię, nigdy nie otwiera untrusted repos i nie woła commit — attack path bywa nieosiągalny. False negative: GitPython jest transitive dependency narzędzia CI/CD, dependency updatera albo AI agenta, który klonuje dowolne repozytoria — wtedy pojedynczy package finding ma ogromny blast radius. Szczególnie narażony jest wzorzec PR/repo URL → clone → modify files → index.commit() → push w botach code review, dependency updaterach, AI coding agents i release automation.

Weryfikacja na dwóch poziomach

Nie wystarczy pip freeze | grep GitPython. Component verification: GitPython ≥ 3.1.60. Workflow verification: CI image przebudowany, workery zrestartowane, długowieczne agenty na nowej wersji, brak starego package w cache virtualenv/kontenera, lockfile realnie rozwiązuje 3.1.60+. Dla kontenerów sprawdź runtime image digest, nie tylko manifest repo. Compensating controls do czasu upgrade: nie otwieraj untrusted repos, wyłącz workflow wykonujące commit, sandboxuj proces, używaj disposable workers, ogranicz sekrety joba, nie montuj host SSH credentials do build container.

Idealny temat na VEX

Dla jednej organizacji: GitPython present + untrusted repos: no + index.commit(): unreachable → not exploitable. Dla innej: GitPython present + public repo URLs accepted + index.commit() reachable + secrets mounted → high business risk. Ta sama paczka i wersja, zupełnie inna realna ekspozycja. Jeśli system przetwarzał attacker-controlled repos i wykonywał commits przed patchem, warto przejrzeć CI logs, child processes przy commit, zmiany filesystem, nowe dostępy do credentiali, egress i artifact tampering — samo skasowanie repo nie wystarcza, jeśli hook już się wykonał.

Powiązane na blogu

Źródła

Nota redakcyjna: stan informacji 10 września 2026. CVE-2026-87817 (GitPython < 3.1.60): impersonacja git directory prowadzi do wykonania pre-commit hooka przy index.commit(). Artykuł podkreśla reachability (które workflowy dochodzą do commitu untrusted repo) i dwupoziomową weryfikację, nie tylko numer paczki.

Zapamiętaj jedno

Repozytorium powinno być danymi wejściowymi, ale manipulacja strukturą Gita potrafi zdecydować, skąd biblioteka pobierze executable hook. Szukaj nie tylko GitPython < 3.1.60, ale workflowów, w których untrusted repo przechodzi aż do index.commit().