One git push, Full Server: CVE-2026-3854 Let Anyone with Push Access RCE GitHub.com and Enterprise Server

Introduction

Wiz Research and GitHub disclosed CVE-2026-3854, a critical command-injection bug in GitHub's internal git push pipeline that let any authenticated user with push access to a single repository run arbitrary commands on GitHub's backend with one ordinary git push -o command. GitHub.com, GitHub Enterprise Cloud, GHEC with Data Residency, GHEC with Enterprise Managed Users, and GitHub Enterprise Server were all affected. GitHub patched the cloud surface on March 4, shipped GHES patches on March 10, and the full technical write-up was published this week — meaning every self-hosted GHES instance that has not been upgraded is now sitting on a public exploit chain.

What Happened

When a git push arrives at GitHub, it flows through several internal services: babeld accepts the SSH/HTTPS connection, gitauth checks policy, and gitrpcd then invokes the pre-receive hook binary that enforces server-side rules. Those services talk to each other via an internal HTTP-style header called X-Stat, a semicolon-separated list of key=value pairs that carries security-relevant metadata such as the Rails environment, hook directories, file-size limits, and whether the request runs in enterprise mode.

Wiz noticed that user-supplied push options (git push -o key=value) were embedded directly into X-Stat without escaping, and the parser used last-write-wins semantics. That meant a semicolon inside a push option value let the attacker terminate the legitimate field and append entirely new internal metadata that downstream services then trusted as if it had come from babeld itself.

The full chain stitches three injected fields together against the pre-receive binary:

  1. rails_env=anything-but-production — drops the request out of the sandboxed production code path into a non-production path that runs custom hooks directly.
  2. custom_hooks_dir=/some/path — tells the binary where to look for hook scripts.
  3. repo_pre_receive_hooks=[{...}] — supplies a JSON hook entry whose script field uses a path-traversal payload, so when the binary joins the redirected base directory with that traversal, it lands on an arbitrary executable on disk and runs it as the git service user.

On GHES, that path is reachable by default. On GitHub.com, custom hooks are normally not invoked because an "enterprise mode" boolean flag is false — but that flag is itself carried in X-Stat and was equally injectable, so one extra crafted field made the same chain work in production as well. Because GitHub.com uses shared storage nodes across tenants, code execution as the git user there meant cross-tenant exposure to other organisations' repositories on the same node. Wiz reported the bug on March 4 at 17:45 UTC; GitHub had a fix live by 19:00 UTC the same day.

Why It Matters

A git push is the single most ubiquitous developer interaction with GitHub on Earth. The exploit needed nothing more exotic than push access to a repository the attacker created themselves and a stock git client. There is no malware, no second-stage payload, no need to keep a session alive — one command and you are running code on shared storage.

For self-hosted GitHub Enterprise Server operators, that translates directly into "anyone with a service account that can push to any repo can take over the whole appliance," which is functionally most of your engineering org plus every CI service principal. The cross-tenant blast radius on GitHub.com makes this a useful case study for the broader pattern Wiz called out: when several services in different languages share an internal protocol, every assumption about that protocol becomes load-bearing security.

Who Is Affected

  • GitHub.com, GitHub Enterprise Cloud, GHEC with Data Residency, and GHEC with Enterprise Managed Users — already patched, no customer action required.
  • GitHub Enterprise Server prior to 3.14.25, 3.15.20, 3.16.16, 3.17.13, 3.18.8, 3.19.4, or 3.20.0. Wiz estimated about 88% of GHES instances were still vulnerable at public disclosure.
  • Any service principal, CI runner, or human user with push rights to any repo on a vulnerable GHES instance — exploitation does not require admin or even repo-owner privileges.

How to Protect Yourself

If you run GitHub Enterprise Server, upgrade now and verify the running version actually matches what you intended:

ssh -p 122 [email protected] \
  'ghe-version'

# fixed releases:
# 3.14.25 / 3.15.20 / 3.16.16 / 3.17.13 / 3.18.8 / 3.19.4 / 3.20.0 (or later)

# trigger the upgrade workflow per GitHub's runbook
# (hot patches available for some minors via ghe-upgrade)
ssh -p 122 [email protected] 'ghe-upgrade /path/to/github-enterprise-<release>.pkg'

Until you have upgraded, hunt the audit log for the highest-signal indicator: a semicolon in a push option value. Legitimate clients almost never put ; in -o values, but the exploit must.

ssh -p 122 [email protected] \
  'sudo zgrep -E "push_options=[^;\" ]*;|push_option_[0-9]+=[^;\" ]*;" \
       /var/log/github-audit.log* | head -200'

A more permissive (and noisier) hunt looks for the names of the injected fields landing in audit records:

sudo zgrep -E 'rails_env=|custom_hooks_dir=|repo_pre_receive_hooks=|user_operator_mode=' \
  /var/log/github-audit.log*

Anything that matches in a git_push event from outside GitHub's own service principals is worth treating as compromise until you have proven otherwise. Snapshot disk and memory, then assume read/write access to repos on that instance — rotate any secrets present in repo files, GitHub Actions, or hooks.

For day-to-day hardening of Enterprise Server appliances, narrow git push access wherever it does not need to be open:

# require branch protection + signed commits on every default branch
gh api -X POST -H "Accept: application/vnd.github+json" \
  /repos/ORG/REPO/branches/main/protection \
  -f required_signatures=true \
  -f enforce_admins=true \
  -F required_status_checks='{"strict":true,"contexts":["ci/required"]}'

# audit which apps/PATs/SSO-issued OAuth tokens have push scope at all
gh api /orgs/ORG/installations | jq '.installations[] | {app: .app_slug, perms: .permissions, suspended: .suspended_at}'

For the cloud products you do not run, the only remaining hygiene is the audit log: GitHub recommended customers review their org-level audit log out of an abundance of caution even though forensics found no malicious exploitation pre-disclosure.

gh api -H "Accept: application/vnd.github+json" \
  "/orgs/ORG/audit-log?phrase=action:git.push+created:2026-02-01..2026-03-05&per_page=100" \
  | jq '.[] | {actor, repo, created_at, push_size}'

If you operate any internal multi-service protocol of your own — gRPC, custom HTTP headers, message-bus envelopes — take this disclosure as a prompt to fuzz delimiter handling end-to-end. Wiz's takeaway is the right one: when service A trusts service B's framing, every parser in the chain becomes part of your authentication boundary.

Source