Wednesday, April 29, 2026
๐Ÿ›ก๏ธ
Adaptive Perspectives, 7-day Insights
Technology

How a Single Semicolon Got Past GitHub's Push Pipeline

Wiz researchers found a critical RCE in GitHub's git push pipeline on March 4. The fix on github.com landed 75 minutes later. The April 28 disclosure makes clear what nearly happened โ€” and why GitHub Enterprise Server admins still have urgent work.

How a Single Semicolon Got Past GitHub's Push Pipeline
Created with OpenAI gpt-image-2.

Note: This post was written by Claude Opus 4.7. The following is a synthesis of GitHub’s security advisory, Wiz Research’s technical writeup, the GitHub Advisory Database entry, and reporting from The Hacker News, Security Affairs, and SocRadar.

On March 4, 2026 at 5:45 PM UTC, researchers at Wiz reported a critical remote code execution vulnerability through GitHub’s Bug Bounty program. By 7:00 PM UTC the same day, GitHub had validated the finding and deployed a fix to github.com. CVE-2026-3854 was assigned on March 10 with a CVSS score of 8.7, and Enterprise Server patches shipped the same day. GitHub held public disclosure for forty-five days, then published the writeup on April 28.

The vulnerability affected github.com, all variants of GitHub Enterprise Cloud, and every supported version of GitHub Enterprise Server. GitHub said Wiz’s report “will receive one of the highest rewards in the history of our Bug Bounty program.” Neither party disclosed the amount.

What this meant for customers

If you push code to github.com, the practical picture is straightforward. The patch landed about 75 minutes after the bug was reported, and GitHub’s forensic investigation found no exploitation outside the researchers who reported it. You did not have to do anything โ€” the managed service was patched automatically.

That picture hides one small uncertainty and one large one. GitHub did not disclose when the vulnerable code was introduced or how far back its telemetry queries reached; “no exploitation” should be read as “no anomalous code path execution within the period we examined.”

The larger uncertainty is GitHub Enterprise Server, the on-premises product used by regulated industries that need data to stay inside their own networks. At public disclosure on April 28, Wiz’s data indicated 88% of GHES instances were still vulnerable.

The mechanism

A git push accepts arbitrary push options via the -o flag. Inside GitHub’s infrastructure, those option values were passed by the babeld git proxy into a semicolon-delimited internal header called X-Stat โ€” roughly field1=value1; field2=value2; ... โ€” and the pre-receive hook runner downstream parsed that header and trusted its contents.

The bug was that babeld did not strip semicolons from user-supplied option values before embedding them. A single unsanitized semicolon was enough to break out of the legitimate value field; everything after it became new entries in the header. A push option whose value contained ; rails_env=development; custom_hooks_dir=/tmp/...; repo_pre_receive_hooks=... injected three new fields into X-Stat. The hook runner, written in a different language than babeld and operating on different assumptions about the data, honored them: rails_env flipped execution out of sandboxed mode, custom_hooks_dir redirected hook lookup to an attacker-controlled path, and repo_pre_receive_hooks defined what would run.

The result was command execution as the git service user on a node that, on github.com, held repositories from unrelated tenants on shared storage. Wiz did not enumerate other organizations’ code, but verified through permission analysis that they could have.

The exploit required push access. On github.com that means anyone with a free account who pushes to their own repository โ€” a trivial bar. On GHES it means an authenticated user inside the deploying organization, a smaller attacker pool that still includes any compromised employee account or contractor.

Cloud was easy. GHES wasn’t.

Wiz’s 88% figure is what makes this story load-bearing for IT teams. Six weeks after the patches shipped, most Enterprise Server installations had not applied them. That is consistent with how on-prem patch cycles work: GHES upgrades require a maintenance window and often coordination with internal CI. The patched releases โ€” 3.14.25, 3.15.20, 3.16.16, 3.17.13, 3.18.7, 3.19.4, and 3.20 โ€” all need an admin to push them.

The exposure on a vulnerable GHES instance is bounded to the repositories on that one box, but for a company that hosts proprietary source on GHES precisely because the cloud is not the right place for it, the threat model still bites: a single compromised developer account becomes RCE on the server.

If you run GHES, upgrade. GitHub’s guidance for admins who suspect anything is to first review /var/log/github-audit.log for push operations containing semicolons in push options.

The trust boundary that broke

Wiz’s framing of the root cause is the part of the writeup worth reading even if the CVE does not apply to your stack: “When multiple services written in different languages pass data through a shared internal protocol, the assumptions each service makes about that data become a critical attack surface.”

Each component of GitHub’s push pipeline made a reasonable choice in isolation. babeld embedded option values into an internal header without sanitizing semicolons, because semicolons are not normally a problem at the network edge. The pre-receive hook runner trusted X-Stat fields without re-validation, because X-Stat is an internal header. The hook runner honored rails_env because that is what the variable does. The bug emerged at the seams between those services โ€” at the trust boundary that nobody owned.

Internal protocols accumulate the same kind of trust debt as public APIs, but with no external eyes on the contract and no fuzzing infrastructure pointed at them. The people who know what fields the protocol expects are the people who built it, and they tend to assume their own internal callers are well-behaved. If you operate any other system that passes user input through an internal protocol with delimiter-based parsing โ€” and most of us do โ€” the writeup to read is Wiz’s, not GitHub’s.

The lesson is that internal headers are not internal in any meaningful sense once they carry user-controlled data.

Sources