On April 28, 2026, the Wiz Research team published a full breakdown of a vulnerability they had reported to GitHub almost two months earlier. The verdict was harsh: a single git push command, hand-crafted with a standard git client, was enough to achieve remote code execution (RCE) on GitHub.com's backend servers and on any unpatched GitHub Enterprise Server (GHES) instance. The vulnerability was assigned CVE-2026-3854 with a CVSS score of 8.7.
This article is a two-layered breakdown. The first part is written for non-technical readers who need to decide quickly whether they have anything to do and what exactly. The second part dissects the exploitation chain the way Wiz reconstructed it, plus a discussion of what this class of bug means for any modern multi-service architecture. At the end, we look at how the Pragma Core platform addresses exactly the kind of problems that made this breach possible.
Part I. Executive breakdown
What happened
Researchers at Wiz uncovered a command injection issue in how GitHub processes git push operations internally. Specifically, the values a user can pass via git push -o (push options) were not properly sanitized before being embedded in an internal header called X-Stat. That header is consumed by downstream services to make security decisions, such as whether to run hooks inside a sandbox or directly, which directory to look in for hook scripts, and what limits to apply to files.
Because the delimiter used in X-Stat (the ; character) could also appear in user input, an attacker could inject extra fields into the header and effectively overwrite the server's security configuration during their own push. By chaining three such injections, the attacker ended up executing arbitrary code on the server processing the push, running as the platform's git user.
Who is affected
| Component | Status |
|---|---|
| GitHub.com | Patch deployed within 2 hours of the report. No user action required. |
| GitHub Enterprise Cloud (all variants) | Mitigated by GitHub. |
| GitHub Enterprise Server (GHES) <= 3.19.1 | Vulnerable. Urgent upgrade required. |
| Patched GHES versions | 3.14.25, 3.15.20, 3.16.16, 3.17.13, 3.18.7, 3.19.4, 3.20.0 or later |
The number that should worry anyone running a self-hosted GHES: at the time of Wiz's publication, 88% of instances were still unpatched. That means thousands of organizations effectively have their entire intellectual property and all their internal secrets exposed to an exploit that only requires push access to a repository, including a repository the attacker creates themselves.
Why this matters beyond GitHub
The bug itself is elegant and relatively easy to understand once you see the diagram. What matters more is the pattern. Many organizations run architectures where services written in different languages (Go, Ruby, Rust, C) pass context to each other through internal headers or metadata structures. Each service makes reasonable assumptions about what the input looks like. When those assumptions don't line up, you get exactly this kind of injection. GitHub's git push pipeline is an almost textbook illustration of the problem: babeld assumed push option values were safe to embed verbatim, gitrpcd assumed everything in the X-Stat header was set by a trusted source, and the pre-receive hook binary assumed the rails_env environment variable could only be production in production.
Each assumption, taken individually, was plausible. Combined, they opened a path to RCE.
Recommended actions
- For self-hosted GHES: upgrade immediately to one of the patched versions. There is no documented workaround. If the upgrade can't happen in the next few days, isolate the instance at the network level and reduce push permissions until the patch lands.
- Check your audit logs. GitHub recommends inspecting
/var/log/github-audit.logfor push operations containing the;character in push options. Any historical entry of that kind deserves investigation as a possible exploitation attempt. - Re-evaluate push access policies. The vulnerability could be exploited by any authenticated user with push permission on a repository, including a repo the attacker just created. If your instance allows free user registration, your attack surface was effectively the full size of your user base.
- Look at the rest of your SDLC. If you run other multi-service platforms (CI/CD systems, registries, build pipelines), apply the same question: what user-controlled input ends up in internal headers or metadata structures without strict sanitization?
Part II. Technical breakdown
The architecture of GitHub's git push pipeline
When a user runs git push over SSH against GitHub, the request flows through a few key components, each written in a different language:
babeld: git proxy and entry point for all SSH-based git operations. It receives the SSH connection, forwards authentication togitauth, and constructs the internalX-Statheader carrying the security metadata applicable to the session.gitauth: internal authentication service. It verifies credentials, checks repository permissions, and returns tobabeldthe policies that apply (file size limits, branch naming rules, and so on).gitrpcd: internal RPC server. It receives the request frombabeld, parsesX-Stat, and sets up the environment for downstream processes. It performs no authentication of its own. It trustsbabeldcompletely.- Pre-receive hook: a compiled Go binary that enforces security policies before a push is accepted. It checks limits, rules, LFS integrity, and runs admin-defined custom hooks.
The critical link between these components is the X-Stat header. It carries configuration fields as key=value pairs separated by semicolons (;). Internal services parse it by splitting on ; and populating a map. And here lies the fatal detail: the map uses last-write-wins semantics. If a key appears twice, the later value silently overrides the earlier one, with no warning.
The vulnerability: X-Stat field injection
Push options are a standard feature of the git protocol. Users can send them with git push -o key=value. babeld encodes them in the X-Stat header as push_option_0, push_option_1, and so on, alongside push_option_count.
The problem: babeld copies push option values directly into X-Stat without escaping the ; character. Because ; is the header's field delimiter, any push option containing this character breaks out of its own field and creates new, attacker-controlled fields.
Concretely, a push option of the form x;large_blob_rejection_enabled=bool:false ended up in the header like this:
X-Stat: ...; large_blob_rejection_enabled=bool:true; ...;
push_option_0=x;large_blob_rejection_enabled=bool:false;
push_option_count=1; ...
After parsing, thanks to last-write-wins semantics, the resulting map looked like:
push_option_0 = x
large_blob_rejection_enabled = bool:false <-- the injected value wins
push_option_count = 1
Wiz confirmed this behavior both through binary analysis and via packet capture on a live GHES instance. Injected fields actually appeared in X-Stat and overrode the values set by babeld.
Security-relevant fields
Through binary analysis of the pre-receive hook, Wiz identified the injectable fields with the highest impact:
| Field | Purpose |
|---|---|
rails_env |
Controls hook execution path (sandbox vs. direct exec) |
custom_hooks_dir |
Base directory for hook script lookup |
repo_pre_receive_hooks |
JSON definitions of hooks to execute |
large_blob_rejection_enabled |
Enforces file size limits |
reject_sha_like_refs |
Blocks SHA-like branch names |
user_operator_mode |
Enables internal debug output |
The first three are what matters most. Combined, they lead to RCE.
The escalation chain to RCE
GHES supports custom pre-receive hooks, admin-defined scripts that run before a push is accepted. Reverse engineering revealed that the pre-receive binary has two execution paths, controlled entirely by the value of rails_env:
- If
rails_env=production, hooks run inside a sandbox. - For any other value, hooks run directly, no sandbox, no isolation, as the
gituser with full filesystem access.
The only thing separating these two paths is the value of rails_env. And that value is injectable.
The full chain, in three steps:
- Sandbox bypass: inject a non-production
rails_envvalue to switch from the sandboxed production path to the unsandboxed one. - Hook directory redirect: inject
custom_hooks_dirto control where the binary looks up hook scripts. - Hook injection with path traversal: inject
repo_pre_receive_hookswith a crafted entry whosescriptfield contains a path traversal sequence. The binary's path resolution joins the attacker-controlled base directory with the traversal payload and resolves to an arbitrary binary on the filesystem.
The non-production path then executes the resolved binary directly, no arguments, no sandbox, as the git user:
git push -o '<injected fields>' origin master
remote: uid=500(git) gid=500(git) groups=500(git) <-- RCE as the git user
To github.com:user/repo.git
abc1234..def5678 master -> master
Jumping from GHES to GitHub.com
Initial tests against GitHub.com produced no output. Hooks simply did not execute on the custom hooks path. Wiz injected user_operator_mode=bool:true to enable debug output on both platforms and compared the trace.
The difference: GitHub.com had a boolean flag in X-Stat that controlled whether the server ran in "enterprise mode". On GHES, the flag defaulted to true, so the custom hooks path was always active. On GitHub.com it defaulted to false, so under normal conditions that path was never reached.
But the flag was carried in X-Stat too. So it was injectable through the same mechanism. With one extra injected field, the full exploitation chain worked on GitHub.com as well.
Cross-tenant impact on GitHub.com
On GHES, RCE as the git user already means full instance compromise. On GitHub.com, things are worse. GitHub.com is multi-tenant. Repositories belonging to millions of organizations and users are stored on shared backend infrastructure. When Wiz achieved code execution on GitHub.com, they landed on one such shared storage node, running as the git user, which by design has read access to every repository hosted on that node.
Wiz explicitly states they did not access the contents of other tenants' repositories. Validation was done only on their own test accounts, by enumerating index entries, which confirmed that the git user's filesystem permissions would have allowed reading any repository on the node, regardless of owner.
Timeline
| Date | Event |
|---|---|
| 2026-03-04 | Wiz Research discovers the push option injection vulnerability. |
| 2026-03-04 | RCE confirmed on GHES 3.19.1. |
| 2026-03-04 | Report sent to GitHub. |
| 2026-03-04 | GitHub acknowledges receipt. |
| 2026-03-04 | Fix deployed on GitHub.com (under 2 hours from report). |
| 2026-03-10 | CVE-2026-3854 assigned, CVSS 8.7. |
| 2026-03-10 | GHES patch released. |
| 2026-04-28 | Public disclosure. |
A note on the discovery methodology
One detail worth calling out separately: Wiz states they used AI-augmented reverse engineering tooling (specifically IDA MCP) to analyze GitHub's compiled binaries and reconstruct the internal protocol. The team characterizes this CVE as "one of the first critical vulnerabilities discovered in closed-source binaries using AI". Whether or not the claim holds up over time, the message to the AppSec community is clear: the cost barrier for closed-source code auditing is dropping fast, and multi-service pipelines are now firmly in scope as practical targets for researchers with moderate resources.
What we should learn from CVE-2026-3854
A few patterns show up repeatedly in this kind of bug, and they're worth investigating proactively in any codebase:
- Internal protocols based on delimiters, without strict escaping. If a character used as a delimiter can also appear in user input, with no escape or encoding, it's only a matter of time until someone exploits it.
- Last-write-wins on parsed structures. Maps that silently overwrite duplicate values are attacker-friendly. Parsing should explicitly reject duplicate keys, or at least log and alert on them.
- Non-production code paths shipped in production binaries. The direct, unsandboxed hooks path existed in the binary that ships to production. Its presence, gated by a single variable, was an invitation. Code paths disabled by a flag remain attack surface until they are removed at build time.
- Missing path traversal validation on internal metadata inputs. The moment a field from an internal header can influence the resolution of an executable path, validation must happen at the point of execution, not at the point where the value is set.
- Implicit trust boundaries between services.
gitrpcdtrustedbabeldcompletely. That trust was documented nowhere and was not technically validated. Inter-service trust should be explicit and verified, ideally cryptographically.
How Pragma Core addresses this class of problem
Pragma Core is an application security platform built specifically for the class of issues that made CVE-2026-3854 possible. We're talking about vulnerabilities that classical scanners don't surface, because they're not about a single badly written function. They're about how input flows across multiple services and components, each carrying its own assumptions about what the others are sending it.
Here are the concrete connections between what the platform does and the bug class this CVE represents:
Autonomous AI agents for attack chain investigation
The autonomous agents in Pragma Core don't stop at "we found an unescaped input". Their reasoning over attack chains tries to answer questions like: "If the value of this parameter ends up in an internal header, where else is the header parsed? Which of the services consuming it have higher privileges than the entry point?". That's exactly the kind of thinking that led to the discovery of CVE-2026-3854.
Interactive call graphs with vulnerability overlay
Wiz's reverse engineering manually reconstructed the call graph between babeld, gitauth, gitrpcd, and the pre-receive hook. Pragma Core generates call graphs automatically for any connected repository, visualizing how classes, functions, and calls connect across the codebase. Overlaying identified vulnerabilities on top of these graphs lets teams see at a glance where untrusted input propagates and where it crosses trust boundaries.
Continuous tracking of third-party packages
Many organizations run libraries and components that propagate the same kinds of bugs into their own pipelines. Pragma Core tracks every third-party package across all connected repositories, surfacing vulnerable versions with CVSS scores and fixed upgrade paths. For a CVE like this one, the practical effect is that an organization self-hosting GHES with internal tooling that depends on the affected version gets notified the moment the CVE lands in the catalog, with no manual monitoring required.
Full SBOM per repository
Pragma Core generates a complete inventory of libraries, versions, licenses, and package URLs for every repository, exportable as CycloneDX JSON. In the context of a breach like CVE-2026-3854, that kind of inventory is essential to quickly answer "what installations do we have, and which version is each running?", which, in 88% of the cases reported by Wiz, was a question teams couldn't answer fast enough.
Human-guided AppSec investigations
The expert-led research module in Pragma Core is built precisely for the type of investigation Wiz did here. An AppSec operator drives deeper analysis, supported by autonomous agents, automated scans, and the platform context already available in the workspace. The focus is on what your team feels is fragile: sensitive features, complex code paths, business-critical components. Not a separate black-box engagement, but a natural extension of your existing subscription, leveraging your existing repository coverage and findings. That's the same way of working that, applied systematically to a pipeline like GitHub's, would have raised flags on the X-Stat injection long before anyone reached the full exploitation chain.
Native repository integration
The platform connects to GitHub, GitLab, and Azure DevOps in minutes. For an organization self-hosting GHES, integration is direct: continuous scanning starts immediately, and the findings generated are contextualized with repository, branch, and commit.
Closing thoughts
CVE-2026-3854 is not, fundamentally, a bug about git push, and it's not a bug about GitHub. It's a bug about what happens when user-controlled input reaches internal protocols based on delimiters, when services make implicit assumptions about each other without verification, and when "non-production" code paths remain present in production binaries.
The same patterns are present in many modern architectures. The difference between an organization that reads this article as a curiosity and one that uses it as a trigger for an audit usually comes down to how mature the application security function is and how much visibility teams have into their own code.
For organizations that want to move from "we scan and report" to "we systematically investigate what's fragile", Pragma Core was built to make that step. You can reach us directly at pragma-core.com for a demo or to talk about how the platform could fit into your security pipeline.
Sources
- Wiz Research, Securing GitHub: Wiz Research uncovers Remote Code Execution in GitHub.com and GitHub Enterprise Server (CVE-2026-3854), April 28, 2026.
- GitHub Security Blog, Securing the git push pipeline: Responding to a critical remote code execution vulnerability, April 29, 2026.
- The Hacker News, Researchers Discover Critical GitHub CVE-2026-3854 RCE Flaw Exploitable via Single Git Push, April 29, 2026.
- Help Net Security, 88% of self-hosted GitHub servers exposed to RCE, researchers warn (CVE-2026-3854), April 29, 2026.
- SOCRadar, CVE-2026-3854 Exposes a Critical Weak Point in GitHub's Git Push Pipeline, April 29, 2026.
- NVD, CVE-2026-3854 Detail.