PYSEC-2026-3835
Vulnerability from pysec - Published: 2026-09-10 09:45 - Updated: 2026-09-10 11:02Summary
GitHacker through 1.1.7 did not validate path segments parsed from attacker-controlled .git/HEAD before joining them onto its output directory. A malicious server could coerce GitHacker into reading arbitrary local files. Contents do not stream back wholesale, but the recovery loop turns any 40-character hex substring into an outbound HTTP GET — an existence oracle for arbitrary paths plus hex-fragment exfiltration of file contents.
Details
Vulnerability
GitHacker rebuilds a remote .git/ by fetching files into temp_dst. Two functions derived filesystem paths from server-controlled content:
add_head_file_tasksreads the downloaded.git/HEAD, parsesref: <ref-path>, and joins the raw ref-path ontotemp_dst/.git/logs/before reading the resulting file.add_hashes_parsedscans any file it reads for 40-character hex substrings and emitsGET .git/objects/<sha[0:2]>/<sha[2:]>for each one — onto the attacker's server and into the local output tree.
Pre-fix, add_head_file_tasks did not validate the ref segments. A malicious .git/HEAD of
ref: ../../../../../../etc/passwd
caused add_head_file_tasks to traverse out of temp_dst and read /etc/passwd. The bytes flowed into add_hashes_parsed, which emitted one outbound HTTP request per 40-char-hex match — observable on the attacker's logs.
Impact
PR #65 originally classified this as arbitrary local file read. Joint analysis during coordinated disclosure narrowed the primitive: file contents do not stream back wholesale because the only egress channel is the 40-char-hex regex. In practice an attacker can:
- Existence oracle for any path on the GitHacker host (
/etc/shadow,/root/.ssh/id_rsa,/home/<user>/.git-credentials, build artifacts under/tmp/). - Hex-fragment exfiltration when the targeted file contains 40-char hex sequences: other git repos' refs / pack filenames, password hashes, HMAC-SHA1 outputs, some session tokens.
Not exploitable without victim action: the attacker must persuade the victim to run GitHacker against a URL they control. Project guidance has always been to run GitHacker inside a disposable container.
Scoping note: no write-side primitive in 1.1.7
A working write-side primitive (attacker drops content outside temp_dst via add_folder / add_task) does not reproduce against the shipped 1.1.7 source. Empirical testing by the reporter against GitHacker-1.1.7.tar.gz (25 traversal-style payloads including ....//, %2e%2e%2f, layered foo/../../, NUL bytes, backslash variants, absolute paths) yielded 0/25 escapes. Two structural reasons:
add_folderanchors every derived path onself.url + '.git/', so the first path component after theurl_lengthstrip is always.git.os.path.join's absolute-path short-circuit never fires.- Python's
str.replace("..", "")is greedy non-overlapping;....//collapses to//,......//to///, etc. No literal..survives intoadd_task.
5f2a8ba is still the correct fix for the read-side primitive and additionally hardens add_task as defense in depth against future regressions — for example, if a later caller removes the .git/ anchor in add_folder or wires a new server-controlled segment source into add_task.
Fix
Commit 5f2a8ba introduces _is_safe_path_segment as a single trust boundary: every segment about to be joined onto temp_dst or appended to an outgoing URL is validated against an allowlist before add_task accepts it. Empty / . / .. / separators / NUL / control characters are rejected; the brittle replace("..", "") filter is removed.
The fix also tightens adjacent surfaces preemptively:
add_folderswitches tourlparse-based scheme + netloc + path comparison.construct_url_from_path_componentspercent-encodes every segment.
PR #65's two-layer defense (allowlist regex + os.path.realpath() confinement) was consolidated onto the allowlist applied at queue-time, removing the TOCTOU window an after-the-fact realpath() check leaves open and the per-call-site drift risk. PR #65 was closed in favour of the broader fix.
Regression tests in tests/test_ref_validation.py (commit 16fcd81) pin the PoC and six bypass variants (extra-depth, mid-path, NUL, absolute path, leading-dot, .lock-suffix).
Credit
Reported and patched-prototyped by Zac Wang (@7a6163) in #65. Zac refined the impact framing from "arbitrary file read" to "existence oracle + hex-fragment exfiltration" and verified the absence of a write-side primitive against the shipped 1.1.7 sdist with a 25-payload harness.
Patches
Patched in 1.1.8 (commit 5f2a8ba; tests 16fcd81).
Workarounds
Run GitHacker inside a disposable container. Do not point GitHacker at any URL whose contents are not under your control.
Resources
- https://github.com/WangYihang/GitHacker/pull/65
- https://github.com/WangYihang/GitHacker/commit/5f2a8ba
- https://github.com/WangYihang/GitHacker/blob/main/tests/test_ref_validation.py
- https://githacker.pages.dev/security
- https://github.com/justinsteven/advisories
- https://drivertom.blogspot.com/2021/08/git.html
| Name | purl | githacker | pkg:pypi/githacker |
|---|
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "githacker",
"purl": "pkg:pypi/githacker"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.1.8"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"1.0.1",
"1.0.10",
"1.0.11",
"1.0.2",
"1.0.3",
"1.0.4",
"1.0.5",
"1.0.6",
"1.0.7",
"1.0.8",
"1.0.9",
"1.1.0",
"1.1.1",
"1.1.3",
"1.1.4",
"1.1.6",
"1.1.7"
]
}
],
"aliases": [
"CVE-2026-50024",
"GHSA-hr3m-4qwq-3mgc"
],
"details": "## Summary\n\nGitHacker through 1.1.7 did not validate path segments parsed from attacker-controlled `.git/HEAD` before joining them onto its output directory. A malicious server could coerce GitHacker into reading arbitrary local files. Contents do not stream back wholesale, but the recovery loop turns any 40-character hex substring into an outbound HTTP `GET` \u2014 an existence oracle for arbitrary paths plus hex-fragment exfiltration of file contents.\n\n## Details\n\n### Vulnerability\n\nGitHacker rebuilds a remote `.git/` by fetching files into `temp_dst`. Two functions derived filesystem paths from server-controlled content:\n\n- `add_head_file_tasks` reads the downloaded `.git/HEAD`, parses `ref: \u003cref-path\u003e`, and joins the raw ref-path onto `temp_dst/.git/logs/` before reading the resulting file.\n- `add_hashes_parsed` scans any file it reads for 40-character hex substrings and emits `GET .git/objects/\u003csha[0:2]\u003e/\u003csha[2:]\u003e` for each one \u2014 onto the attacker\u0027s server and into the local output tree.\n\nPre-fix, `add_head_file_tasks` did not validate the ref segments. A malicious `.git/HEAD` of\n\n```\nref: ../../../../../../etc/passwd\n```\n\ncaused `add_head_file_tasks` to traverse out of `temp_dst` and read `/etc/passwd`. The bytes flowed into `add_hashes_parsed`, which emitted one outbound HTTP request per 40-char-hex match \u2014 observable on the attacker\u0027s logs.\n\n### Impact\n\nPR #65 originally classified this as arbitrary local file read. Joint analysis during coordinated disclosure narrowed the primitive: file contents do not stream back wholesale because the only egress channel is the 40-char-hex regex. In practice an attacker can:\n\n- **Existence oracle** for any path on the GitHacker host (`/etc/shadow`, `/root/.ssh/id_rsa`, `/home/\u003cuser\u003e/.git-credentials`, build artifacts under `/tmp/`).\n- **Hex-fragment exfiltration** when the targeted file contains 40-char hex sequences: other git repos\u0027 refs / pack filenames, password hashes, HMAC-SHA1 outputs, some session tokens.\n\nNot exploitable without victim action: the attacker must persuade the victim to run GitHacker against a URL they control. Project guidance has always been to run GitHacker inside a disposable container.\n\n### Scoping note: no write-side primitive in 1.1.7\n\nA working write-side primitive (attacker drops content outside `temp_dst` via `add_folder` / `add_task`) does **not** reproduce against the shipped 1.1.7 source. Empirical testing by the reporter against `GitHacker-1.1.7.tar.gz` (25 traversal-style payloads including `....//`, `%2e%2e%2f`, layered `foo/../../`, NUL bytes, backslash variants, absolute paths) yielded 0/25 escapes. Two structural reasons:\n\n1. `add_folder` anchors every derived path on `self.url + \u0027.git/\u0027`, so the first path component after the `url_length` strip is always `.git`. `os.path.join`\u0027s absolute-path short-circuit never fires.\n2. Python\u0027s `str.replace(\"..\", \"\")` is greedy non-overlapping; `....//` collapses to `//`, `......//` to `///`, etc. No literal `..` survives into `add_task`.\n\n`5f2a8ba` is still the correct fix for the read-side primitive and additionally hardens `add_task` as defense in depth against future regressions \u2014 for example, if a later caller removes the `.git/` anchor in `add_folder` or wires a new server-controlled segment source into `add_task`.\n\n### Fix\n\nCommit [`5f2a8ba`](https://github.com/WangYihang/GitHacker/commit/5f2a8ba) introduces `_is_safe_path_segment` as a single trust boundary: every segment about to be joined onto `temp_dst` or appended to an outgoing URL is validated against an allowlist before `add_task` accepts it. Empty / `.` / `..` / separators / NUL / control characters are rejected; the brittle `replace(\"..\", \"\")` filter is removed.\n\nThe fix also tightens adjacent surfaces preemptively:\n\n- `add_folder` switches to `urlparse`-based scheme + netloc + path comparison.\n- `construct_url_from_path_components` percent-encodes every segment.\n\nPR #65\u0027s two-layer defense (allowlist regex + `os.path.realpath()` confinement) was consolidated onto the allowlist applied at queue-time, removing the TOCTOU window an after-the-fact `realpath()` check leaves open and the per-call-site drift risk. PR #65 was closed in favour of the broader fix.\n\nRegression tests in [`tests/test_ref_validation.py`](https://github.com/WangYihang/GitHacker/blob/main/tests/test_ref_validation.py) (commit [`16fcd81`](https://github.com/WangYihang/GitHacker/commit/16fcd81)) pin the PoC and six bypass variants (extra-depth, mid-path, NUL, absolute path, leading-dot, `.lock`-suffix).\n\n### Credit\n\nReported and patched-prototyped by **Zac Wang** ([@7a6163](https://github.com/7a6163)) in [#65](https://github.com/WangYihang/GitHacker/pull/65). Zac refined the impact framing from \"arbitrary file read\" to \"existence oracle + hex-fragment exfiltration\" and verified the absence of a write-side primitive against the shipped 1.1.7 sdist with a 25-payload harness.\n\n## Patches\n\nPatched in 1.1.8 (commit [`5f2a8ba`](https://github.com/WangYihang/GitHacker/commit/5f2a8ba); tests [`16fcd81`](https://github.com/WangYihang/GitHacker/commit/16fcd81)).\n\n## Workarounds\n\nRun GitHacker inside a disposable container. Do not point GitHacker at any URL whose contents are not under your control.\n\n## Resources\n\n- https://github.com/WangYihang/GitHacker/pull/65\n- https://github.com/WangYihang/GitHacker/commit/5f2a8ba\n- https://github.com/WangYihang/GitHacker/blob/main/tests/test_ref_validation.py\n- https://githacker.pages.dev/security\n- https://github.com/justinsteven/advisories\n- https://drivertom.blogspot.com/2021/08/git.html",
"id": "PYSEC-2026-3835",
"modified": "2026-09-10T11:02:06.985552Z",
"published": "2026-09-10T09:45:01.208337Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/WangYihang/GitHacker/security/advisories/GHSA-hr3m-4qwq-3mgc"
},
{
"type": "WEB",
"url": "https://github.com/WangYihang/GitHacker/commit/5f2a8ba"
},
{
"type": "PACKAGE",
"url": "https://github.com/WangYihang/GitHacker"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/githacker"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-hr3m-4qwq-3mgc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-50024"
}
],
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "GitHacker: Path traversal in ref/hash parsing enables existence oracle and hex-fragment exfiltration via malicious .git server"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.