Blogs
CVEs
2026-3854
Critical · Disclosed
CVE-2026-3854 · CVSS 8.7 · GitHub.com + GHES ≤ 3.19.1

Remote Code Execution in GitHub.com and GitHub Enterprise Server

A header-injection bug in GitHub's internal plumbing turned a routine git push into arbitrary code execution on GitHub's backend - affecting both GitHub.com and on-premise Enterprise Server.
✍️ Lambert
📅 April 28, 2026
12 min read
Severity
8.7
High · Network · Low Privileges
Topics
Security
Web
Reverse Engineering
Header Injection
Original Wiz Write-upGitHub Advisory
When you push code to GitHub, your request travels through several internal services that talk to each other. They share security info using a header where fields are separated by semicolons (;). GitHub forgot to clean semicolons out of one user-controllable input - so an attacker could sneak extra fake fields into that header, override security settings, and trick the system into running their own code on GitHub's servers. All from a regular git push.
8.7
CVSS Score
6 hr
.com Patch Time
3
Injections Chained
~88%
GHES Unpatched at Disclosure

01The Core Idea

At its heart this is a classic header-injection bug hiding inside an internal protocol most users never see. The "envelope" is an internal HTTP header called X-Stat. Its fields look like key=value;key=value;key=value. When a key appears twice, the last one wins - and the attacker controls the last one.

Imagine an office where letters travel between departments inside an envelope stamped with security instructions: "Approved by Reception · Sender: John · Clearance: Low". One department writes those stamps. The next department just trusts whatever's stamped on the envelope. Now imagine you can write extra stamps yourself - "Clearance: Admin" - and sneak them onto the envelope. Every later department reads the last stamp and treats you as an admin.

That's exactly what happened here. Push options sent by the client are interpolated into X-Stat verbatim. No semicolon escaping. No allow-list. Each downstream service trusts the header as if it came from a privileged caller upstream.


02How the Attack Chain Works

The full exploit is three injections collapsed into a single push:

STEP 01
Inject via push options
The attacker passes a crafted value with git push -o. GitHub copies it verbatim into the internal header - semicolons and all.
STEP 02
Override security flags
Injected semicolons spawn fake fields that override real ones. large_blob_rejection_enabled flips, and rails_env shifts away from "production".
STEP 03
Escape the sandbox
That single flag flip routes execution from the sandboxed code path to the unsandboxed one - running as the privileged git service user.
STEP 04
Path-traverse to a binary
Two more injected fields point to a custom hooks directory and a fake "hook" whose path traverses (../../) to any binary the attacker chooses.

The exploit, on screen

What it actually looks like when run against a vulnerable GitHub Enterprise Server:

terminal · attacker@laptop
$ git push -o '<injected fields>' origin master
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 250 bytes | 250.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: uid=500(git) gid=500(git) groups=500(git) ← arbitrary code, running as the git user
To github.com:user/repo.git
   abc1234..def5678  master -> master

03The Header, Forged

What the downstream service thinks it received vs. what's actually in there. Highlighted fields are injected by the attacker; struck-through values are the legitimate ones that get overridden because the parser keeps the last value for a key.

X-Stat: user_id=42; repo=acme/site; push_option="safe"; rails_env=staging; large_blob_rejection_enabled=false; hooks_path=/tmp/x; pre_receive_hook=../../bin/sh
↓ parser splits on ; - last value for each key wins
user_id=42
repo=acme/site
push_option="safe"
rails_env=productionstaging
large_blob_rejection_enabled=truefalse
hooks_path=/tmp/x (attacker-controlled directory)
pre_receive_hook=../../bin/sh (traversal → arbitrary binary)

One legitimate field - push_option - became four illegitimate ones, two of which silently took precedence over real environment configuration. The hook runner happily exec'd whatever path the last fields pointed to.


04Why It Mattered

The blast radius depends on where the bug was triggered. The same primitive lands very differently on shared infrastructure versus a self-hosted appliance.

GitHub.com
Cross-tenant exposure on shared storage
Code execution lands on a shared backend storage node. The git user there has filesystem access to every repository on that node - meaning millions of repos belonging to other organizations and users could have been read.
Wiz did not access other tenants' data. They only confirmed the access permissions allowed it.
Enterprise Server
Full server compromise
On a self-hosted GHES box, the same bug grants complete server takeover: every repository on the appliance, every internal secret, every config file. Game over for that environment.
At time of disclosure, ~88% of GHES instances were still unpatched.

05What To Do

GitHub.com is already fixed - no action needed there. But for self-hosted GHES, the patch is the only thing standing between an authenticated user and full root on your appliance.

  • GitHub.com
    Nothing to do. Mitigated within 6 hours of disclosure by GitHub’s security team.
  • !
    GHES ≤ 3.19.1 - patch now
    Upgrade to 3.19.3 or one of the backported fixes: 3.14.24 · 3.15.19 · 3.16.15 · 3.17.12 · 3.18.6.
  • For internal-services teams
    Audit any internal protocol where one service trusts another’s headers without validation. The X-Stat pattern is everywhere - anywhere a string concat builds an “envelope”, an injection lurks.

06Disclosure Timeline

From discovery to vendor fix in a single day; public disclosure followed once enough customers had upgraded.

2026-03-04 · 00:00 UTC
Wiz Research finds the X-Stat injection bug.
During AI-augmented reverse engineering of GHES binaries with IDA MCP.
2026-03-04
RCE confirmed on GHES 3.19.1. Reported to GitHub.
Coordinated disclosure via GitHub's security@ inbox.
2026-03-04
GitHub deploys fix on GitHub.com - same day.
Roll-out completed within 6 hours of report.
2026-03-10
CVE-2026-3854 assigned. GHES patch released.
Six backported versions for supported release branches.
2026-04-28
Public disclosure. Wiz publishes the technical write-up.
~55 days post-vendor fix.

07The Bigger Lesson

The pattern here goes way beyond GitHub. Modern systems are built from many services, often in different languages, sharing data through internal protocols. Each service makes reasonable assumptions about that data. But reasonable assumptions stacked together can become dangerous gaps.

Here, one service assumed user-controlled push options were safe to embed verbatim. Another assumed every field in X-Stat came from a trusted source. The hook runner assumed rails_env only ever held "production" in production. Each was fine on its own - fatal in combination.

Worth noting: Wiz used AI-augmented reverse engineering (IDA MCP) to analyze GitHub's closed-source binaries. They call this one of the first critical vulnerabilities discovered with that workflow - a hint at where security research is heading.


Read the original write-up
This article is a clarity-focused summary. Wiz Research's full technical post has the binary diff, the parser internals, and the IDA MCP workflow that led them there.
Wiz · CVE-2026-3854
Related Projects
🔓
Understanding MachO Binary Format
Binary internals and security analysis on iOS/macOS
On this page
Loading...
N

Subscribe to my newsletter

Get notified when I publish new posts on my blog

© 2026 Minh-Tri Le. All rights reserved.