CVE-2026-105295 — GNU GitAhead: Unverified Update Installation and TLS Bypass
Unverified update installation and a persistent TLS bypass in GitAhead 2.5.0-2.7.1.
- #GitAhead
- #Updater
- #TLS
- #CWE-494
§Why this one got its own CVE (and why I picked GitAhead in the first place)
Same audit, same tree — commit `1cfc416` (`v2.7.1-12-g1cfc416`), clone at `D:\cve-testing\gitahead`. This is the GitAhead finding I ranked second, behind the `Filter.cpp` bash injection (CVE-2026-106058). Different root cause, so CNA rules mean it gets its own ID rather than being folded into its sibling — a rule I will quote from memory forever now.
And I'd like a little honesty about how this one got on the list to begin with. The classic pattern — *unsigned software updates* — is the single most-accepted remote-RCE bug class across every mature product, so when I saw that GitAhead is a desktop app with an auto-updater and direct-download distribution, my first move was suspiciously predictive: I grepped `src/update/` for `sha256|signature|codesign|verify|authenticode|checksum|certificate` **before I read any updater code properly at all.** Zero hits.
I remember thinking: "either this project is boringly safe, or I have the whole bug class at once." Spoiler: I had the whole bug class at once, and also the grep was doing most of the intellectual work that afternoon.
§The static proof: what I walked through
The PoC script (`poc_gitahead_updater.py`) formalizes what the null grep suggested, by walking a few assertions through `src/update/`:
- 01**Zero integrity verification — as an absence.** Scanning `Updater.cpp` and `Updater_win.cpp`, the keywords above hit nothing. And here's the trick about auditing closure-as-a-bug: at the same time, the script asserts the exec *sinks* exist. The point is the pairing: the verification is missing **and** the execution is present, verified-in-place:
- Linux: `src/update/Updater.cpp:259-279` — `QProcess::execute("/bin/sh", {downloadPath, …})`
- Windows: `Updater_win.cpp:7-28` — `ShellExecuteW` on the downloaded installer
- macOS: `Updater_mac.mm:73-122` — copies a downloaded `.dmg` over the running `.app`, **no codesign check anywhere in the file**
- 01**A deterministic, spoofable download URL.** The updater fetches the changelog (`doc/changelog.md` from raw.githubusercontent.com) via a plain `QNetworkAccessManager.get()` — no certificate pinning — then parses the `### vX.Y` heading and builds the download URL from a format string:
https://github.com/gitahead/gitahead/releases/download/v%1/GitAhead%2-%3.%4Which means one forged changelog heading — `### v99.99` — makes the updater **itself construct and fetch the download URL** for a release the attacker controls. No need for clever ancillary endpoint injection or a bug in URL parsing; the "new version is v99.99, fetch it from here" logic is, itself, the injection point. I found that almost funny when I realized it — the exploit is the feature, exactly as written.
- 01**A persistent TLS bypass.** `src/app/Application.cpp:284-309` — `handleSslErrors` shows a dialog and, on **one** "Ignore" click, does:
settings.setValue("ssl/ignore", true);Permanently. From then on, *all future* update and GitHub API requests silently accept invalid certificates. That is not a per-host memo; that is one click converting GitAhead forever into a tool that trusts whatever certificate appears. I flagged this as a separate contributing problem because it's precisely what makes a hostile network scenario viable **long after the click** that "accepted" it.
- 01**One redirect nuance worth writing down.** The updater uses `NoLessSafeRedirectPolicy` — blocks https→http downgrades, but allows https→https to an attacker's host. Protocol hygiene, in other words, but not effectively sparse enough to save the download once the changelog is attacker-controlled.
§The exploit chain, as it would play out
Vanilla pattern, strung together end-to-end:
- 01Victim is on a hostile network — café Wi-Fi, hotel, corporate MITM — and GitAhead checks for updates on its own schedule. CVSS scores the path `AV:N`, `UI:R`, `AC:H`; the "high complexity" is precisely *you must intercept once and get one click*.
- 02GitAhead's TLS check trips on the attacker's fake cert. The user clicks "Ignore" in the SSL error dialog. `ssl/ignore=true` — and stays true forever, outliving the click that wrote it.
- 03The next changelog fetch is MITM'd. The attacker injects `### v99.99`.
- 04The updater — faithfully, deterministically — builds `https://github.com/gitahead/gitahead/releases/download/v99.99/…` and makes the request itself. From the attacker's MITM chair, the updater is now just a client fetching from wherever the answer comes from.
- 05The downloaded binary runs. On Linux via `/bin/sh`; on Windows via `ShellExecuteW`; on macOS by overwriting the `.app` on the next reboot. **Code execution with no signature check at any point.**
No repo to clone, no zero-day in a parser, no memory corruption — just a software distribution trust chain with the links left out. I personally find this class both the least glamorous and the highest-severity bug class on Earth, and there's something pleasingly dark about a tool auto-updating you into an attack.
§Submission and assignment
It went out as a standalone submission — distinct CWE, distinct root cause from the Filter and ShowTool entries — including the grep-output artifact, the sink locations, the changelog→URL construction, the sticky `ssl/ignore` mechanism, the redirect nuance, and the fix proposal: sign releases (minisign / codesign / Authenticode), verify signature **and** hash before exec, and replace the sticky global SSL bypass with per-host, expiring exceptions (plus pinning for the raw.githubusercontent.com fetch where feasible).
Result:
- **CVE-2026-105295** assigned, CNA VulnCheck, published 2026-10-05
- Title: *"GitAhead 2.5.0 through 2.7.1 Unverified Update Installation and TLS Bypass"*
- Affected: 2.5.0 (2018) through 2.7.1 — verified against the version table; since the project hasn't shipped a release in years, this finding is unaffected-by-fix and therefore *will still be vulnerable for exactly as long as anyone keeps using it*
- Issue #662 in the gitahead repo; technical refs `Updater.cpp#L176-L279` and `Application.cpp#L284-L309`
- CISA SSVC: Exploitation=none · Automatable=no · Technical Impact=total
Worth a precision note: the CNA scored this 7.5 with `AC:H`, very close to my own (7.5–8.1 depending on sink). Close agreement, and both my report and the record rate C/I/A as total if the installed binary runs — which is the point of the exercise.
§Takeaways (or: the lesson I keep re-learning)
For maintainers, the ordering is nearly boring at this point — **sign everything, verify before exec, never persist a user's "ignore" past a single dialog.** But the boring-ness is the point: the updater is the one component where a compromise equals immediate persistent code execution, which is why every mature desktop ecosystem (Sparkle, Squirrel, Windows Update) treats that chain like their crown jewels and still make it tedious to add a hash check.
For auditors — and honestly, this is the part of the blog I most want to be found by someone: **"grep for the absence" on auto-updaters is a great first pass.** Running `sha256|signature|codesign|verify` and finding *nothing* in a component that downloads-and-then-executes is, more often than not, the entire bug. The remaining work is then walking the control chain (changelog → URL → install) to make the attack legible end to end — which, in this case, took an afternoon and a format string.
Written by
MJ — offensive security researcher
I break things in a lab so defenders can build better controls. Everything published here is lab-derived, responsibly disclosed, and documented with the detection path in mind.