CVE-2026-3854: A Single git push Owned GitHub
If you run GitHub Enterprise Server, the answer to "are we exposed?" is almost certainly yes. CVE-2026-3854 is a remote code execution bug in GitHub's git push pipeline that let any authenticated user with push access to a single repository pop a shell on the server. On GitHub.com, the same bug crossed tenant boundaries and exposed millions of repositories on the shared storage nodes. Researchers at Wiz reported it on March 4, 2026. The full technical write-up landed publicly on April 28, 2026, and Help Net Security followed up on April 29 with the headline that 88% of self-hosted GHES instances reachable on the internet were unpatched.
The exploit is one git command. No CVE-of-the-week phishing kit, no exotic protocol abuse. Just a push option containing a semicolon.
Here is what happened, why the bug existed in the first place, and what to do about it on Monday morning.
TLDR
| Detail | Info |
|---|---|
| CVE | CVE-2026-3854 |
| Class | Remote code execution via header injection |
| CVSS | 8.7 |
| Affected | GitHub.com (already patched), GitHub Enterprise Server <= 3.19.3 |
| Reported | March 4, 2026 by Wiz |
| Fixed on github.com | March 4, 2026 (within 75 minutes) |
| Public disclosure | April 28, 2026 |
| Required access | Any authenticated user with push access to one repo |
| Impact (GHES) | Full server compromise, all hosted repositories, internal secrets |
| Impact (github.com) | Cross-tenant read access on shared storage nodes |
| Patched GHES versions | 3.14.25, 3.15.20, 3.16.16, 3.17.13, 3.18.7, 3.19.4, 3.20.0 |
| What you do | Upgrade GHES today, grep audit logs for ; in push options, review the babeld changelog |
What Happened
GitHub's push pipeline has four moving parts:
- babeld, a git proxy that takes the user's SSH connection and forwards it inward.
- gitauth, the service that validates credentials and answers with security metadata (size limits, branch rules, hook config).
- gitrpcd, the internal RPC server that prepares the environment for downstream binaries.
- The pre-receive hook, a compiled Go binary that enforces policy before the push is accepted.
These services talk to each other using a header called X-Stat. It carries security-critical fields as semicolon-delimited key=value pairs. The format uses last-write-wins semantics: if a key appears twice, the second value wins.
That last sentence is the whole bug.
When you run git push -o foo=bar origin main, the value foo=bar is a push option. babeld embeds those user-supplied options into the X-Stat header as push_option_0, push_option_1, and so on. It did not strip semicolons. So if you pushed with a push option that contained one, you could break out of your own field and write a new field that downstream services treated as trusted internal metadata.
Wiz researchers chained three injected X-Stat fields into a clean RCE. Then they noticed an additional field that flipped the same exploit from "GHES on prem" into "we can read other people's repositories on github.com." GitHub deployed a github.com fix in 75 minutes and shipped GHES patches the same day, but the disclosure window meant a lot of self-hosted instances spent two months running unpatched code.
How the Bug Worked
The X-Stat header
X-Stat looks something like this on the wire:
X-Stat: rails_env=production;custom_hooks_dir=/data/hooks;repo_pre_receive_hooks=[]; push_option_0=foo=bar
Each field controls something the downstream services rely on. rails_env decides whether the pre-receive binary runs hooks inside a sandbox. custom_hooks_dir is the base directory hooks are loaded from. repo_pre_receive_hooks is a JSON array of hook scripts to execute. All three are normally set by gitauth based on the authenticated repo and its policies. babeld is supposed to add only the fields the user is allowed to influence (the push options), then forward the header to gitrpcd.
The problem: babeld copied user-supplied push option values verbatim into the header without escaping the field delimiter.
The injection
Imagine you push with a crafted option:
git push -o 'x;rails_env=staging;custom_hooks_dir=/data/user-uploads;repo_pre_receive_hooks=[{"script":"../../../tmp/payload.sh"}]' origin main
babeld serializes that into X-Stat as a single field:
push_option_0=x;rails_env=staging;custom_hooks_dir=/data/user-uploads;repo_pre_receive_hooks=[...]
The downstream parser splits on the semicolon. It sees five fields, not one. Because of last-write-wins, the attacker's rails_env, custom_hooks_dir, and repo_pre_receive_hooks override whatever gitauth set.
The RCE chain
Three overrides combine to give code execution.
rails_env controls the pre-receive binary's two execution paths. With rails_env=production, hooks run inside a sandbox. With anything else, they run directly as the git service user, no sandbox, no isolation.
custom_hooks_dir points at the directory the binary loads hook scripts from. Set it to a directory the attacker can write to. Repository content uploaded as part of the push lives somewhere on disk; pick a directory under that root.
repo_pre_receive_hooks is a JSON array of hook definitions. Each entry has a script field. Path traversal in script resolves the final path against custom_hooks_dir plus the traversal, which lets the attacker land it on any binary they have already managed to write into the repo.
The pre-receive binary then executes that path, no arguments, no sandbox, as git.
The end-to-end output from the Wiz proof of concept looked like this:
$ git push -o '<injected fields>' origin master
remote: uid=500(git) gid=500(git) groups=500(git)
That is shell on a GitHub backend.
The github.com twist
On GHES, the git user has filesystem access to all repositories hosted on the appliance. Game over for that customer. On github.com, an extra injectable field, enterprise_mode, decides which storage backend the pre-receive binary connects to. By forcing it to a specific value, the exploit landed shell on a shared storage node where the git user could read every repository hosted on that node. Wiz confirmed the cross-tenant access using their own accounts and reported it before testing further.
In other words, the same bug that compromised a single GHES appliance also crossed a multi-tenant boundary on github.com. That is unusual, and it is why the CVSS is high despite the "authenticated user with push access" precondition: on github.com, anyone who can register an account and push to a repo they created has push access.
Are You Affected?
github.com
You were exposed for some window before March 4, 2026. GitHub's forensic review concluded there was no exploitation outside the researchers' own testing. No action required from your side.
GitHub Enterprise Server
Check your version:
ssh admin@your-ghes-host -- 'ghe-version'
If you are not on one of the patched releases below, treat the appliance as actively exploitable. Any authenticated user (including a user with access to one repo) can run code on it.
| GHES branch | First patched release |
|---|---|
| 3.14.x | 3.14.25 |
| 3.15.x | 3.15.20 |
| 3.16.x | 3.16.16 |
| 3.17.x | 3.17.13 |
| 3.18.x | 3.18.7 |
| 3.19.x | 3.19.4 |
| 3.20.x | 3.20.0 |
Help Net Security's scan of internet-reachable GHES instances on April 29, 2026 found 88% were on a vulnerable version. If your GHES is exposed to the internet at all, assume someone has already fingerprinted it.
What to Do Right Now
1. Upgrade GHES
This is the only real fix. Apply the patch release for your branch. The upgrade is a hotpatch on most versions, so it is fast:
# On the GHES appliance
ghe-upgrade /path/to/github-enterprise-3.19.4.hpkg
# Verify
ghe-version
If you are more than two minor versions behind, do a staged upgrade through the intermediate releases. GitHub publishes the supported upgrade paths in the GHES upgrade docs; do not skip them.
2. Audit your push logs for the IoC
The exploit requires a semicolon inside a push option. That is not something legitimate tooling produces. Grep the audit log:
sudo zgrep -E 'push_option.*;' /var/log/github/audit.log* | less
Or via the audit log UI, filter for events of type git.push and search for push_option entries that contain ;. Anything that matches is suspicious. Anything from before the patch date and from a user account you do not recognize should be treated as a confirmed compromise indicator, not a maybe.
GitHub also recommends checking for unusual values of the following X-Stat-derived fields in any internal logs you have:
rails_envset to anything other thanproductioncustom_hooks_dirpointing outside/data/user/git-hooksrepo_pre_receive_hookscontaining path traversal sequences (..)
3. Rotate appliance-scoped secrets if you find an IoC
If a push with a ; in push_option predates your upgrade and the user is not who you think, treat the appliance as compromised:
- All deploy keys and machine user tokens
- OAuth app and GitHub App private keys
- Webhook secrets
- Actions runner registration tokens
- LDAP/SAML signing certs and any service-account credentials
- Any cloud credentials stored in repo secrets
The pre-receive binary runs as the git user, which can read every repo on the appliance. Treat secrets that lived in any repo as exposed, not just the repo the attacker pushed to.
4. Review your GitHub App and OAuth scopes
The exploit's blast radius on GHES includes anything the appliance can call out to. If an integration's webhook is signed with a secret stored on the appliance, the secret is in scope. Work outwards from the appliance and trim scopes on connected services that do not need write access.
5. Lock down "authenticated user with push access"
If your GHES allows self-service repository creation, every authenticated user on the appliance has push access to at least one repository (theirs). On a GHES that exposes SSH or HTTPS to the internet, every authenticated user is a potential exploit precondition. Until you have upgraded:
- Disable repository creation for unprivileged users.
- Restrict SSH and HTTPS to the corporate network or a VPN.
- Disable inbound network access to the appliance from anywhere you do not control.
These are not fixes, they shrink the attack surface while you patch.
Why "internal headers" keep biting
CVE-2026-3854 is a textbook header-injection bug, and they keep happening for the same reason: services use a delimiter character that can also appear in user input, and the boundary between "trusted internal metadata" and "user-controlled value" is enforced by an honor system.
A few patterns to learn from this:
Pick a delimiter that cannot appear in untrusted input, or escape it. Semicolons are common in user data (URLs, MIME types, semicolon-separated CSV-ish strings). If you are going to use one as a field separator, you must escape or strip it on the way in. Better, use a length-prefixed binary format or JSON between services and let the parser handle it.
Treat downstream services as if they will trust whatever you send them. babeld assumed gitrpcd would re-validate. gitrpcd assumed gitauth had set the trusted fields. The pre-receive binary assumed both. Nobody re-validated the join. This is the opposite of defense in depth.
Test with adversarial inputs across service boundaries. A unit test for babeld's push-option handling never noticed because the test inputs did not contain ;. A unit test for gitrpcd's X-Stat parser never noticed because the test inputs were synthesized internally. The bug only existed at the seam.
Internal headers deserve the same paranoia as external APIs. "Only our own services talk to it" is not a security control when one of those services accepts user input.
Why This Matters for DevOps Teams
A few things stand out about this CVE beyond the immediate patch:
Self-hosted does not mean low risk. GHES is the version of GitHub that runs inside your perimeter, often behind a VPN, often configured as the canonical source of truth for an organization's code. The appliance also stores deploy keys, webhook secrets, OIDC trust relationships, and Actions runner tokens. A single RCE on the appliance is, for most orgs, equivalent to a full software supply chain compromise.
Push options are user input. Most teams treat git push -o as a tooling channel for things like CI flags or merge-queue annotations. The protocol does not give those values any privileged status; they are user-supplied strings. If you write tools that consume push options server-side, sanitize them like you would any other request body.
Patch latency is the actual risk. GitHub patched github.com in 75 minutes. The same fix took two months to reach 88% of self-hosted instances. That is not a vendor problem; that is the organization owning the appliance not having an upgrade rhythm. Build the cadence before the next CVE.
Your audit logs are the only proof you have. If you do not capture push options today, you cannot answer "were we exploited" for this CVE except by trusting that nobody noticed. Make sure your GHES audit log retention is at least 90 days, that you ship audit events into a SIEM you actually search, and that the retention covers the full disclosure-to-patch window for the vulnerabilities you have not seen yet.
Key Takeaways
- Upgrade GHES today to 3.14.25, 3.15.20, 3.16.16, 3.17.13, 3.18.7, 3.19.4, or 3.20.0.
- Grep audit logs for
;insidepush_optionvalues. That is the IoC. Anything matching from before your upgrade is a treat-as-compromised event. - Rotate appliance-scoped secrets if you find an IoC: deploy keys, App private keys, webhook secrets, runner registration tokens, repo secrets.
- Restrict the attack precondition. If your GHES is internet-reachable, assume someone has fingerprinted it. Move SSH and HTTPS behind a VPN until you patch.
- Build a GHES upgrade cadence. 88% of internet-reachable instances were unpatched two months after the fix shipped. The next CVE is already on someone's disclosure timeline.
- Treat internal service headers like external APIs. Delimiter-based formats with no escaping at the seam are how this bug existed; the seam is where you should test.
The bug itself was a one-character oversight. The two-month window between fix and field deployment is the part DevOps teams own.
Sources: GitHub Security Blog, Wiz Research, The Hacker News, Help Net Security, Cybersecurity News
We earn commissions when you shop through the links below.
DigitalOcean
Cloud infrastructure for developers
Simple, reliable cloud computing designed for developers
DevDojo
Developer community & tools
Join a community of developers sharing knowledge and tools
SMTPfast
Developer-first email API
Send transactional and marketing email through a clean REST API. Detailed logs, webhooks, and embeddable signup forms in one dashboard.
QuizAPI
Developer-first quiz platform
Build, generate, and embed quizzes with a powerful REST API. AI-powered question generation and live multiplayer.
Want to support DevOps Daily and reach thousands of developers?
Become a SponsorFound an issue?