contents [06]
  1. The management API
  2. How the bug worked
  3. The HMAC did not bind the operation
  4. Same bytes, different meaning
  5. PoC
  6. Timeline

In August, I reported a bug in GitHub Enterprise Server (GHES) that started with an unauthenticated hardware check. That check made internal calls to the cluster nodes. I redirected one of those calls and got GHES to return a management API credential as an error message.

I used that credential to register an SSH key for admin through the API, which gave me control over the server.

The management API

GHES is the version of GitHub that an organization runs on its own infrastructure. The Manage API lets you check settings, monitor services, and manage access to GHES.

In a cluster, services run across several nodes. The Manage API gateway receives the client’s request and calls the agents on those machines over RPC. To check the hardware, it calls CheckSystemRequirements and collects the results, including any error messages from the nodes.

How the bug worked

The hardware check accepted a cluster configuration with the node addresses without requiring authentication. The gateway sent CheckSystemRequirements calls to those addresses. With my server on that list, the call reached it carrying the internal authentication GHES used to talk to its agents.

My server returned a redirect to GetSecrets, the internal GHES operation that looked up credentials. The RPC client followed the redirect with the same authentication, which GetSecrets also accepted. That operation’s response included a key with admin access to the Manage API.

On the way back, the gateway treated the GetSecrets response as if it came from CheckSystemRequirements. It read the key as a node’s error message and returned it to whoever had sent the original request. In the diagram, A represents CheckSystemRequirements and B, GetSecrets.

A redirect changes the RPC operation from A to B. The gateway retains its ReplyA decoder, accepts a compatible field from response bB, and returns the credential as an error.
The RPC operation changes while the response decoder remains bound to the original call.

The HMAC did not bind the operation

The gateway authenticated calls to the agents with an HMAC that covered only the timestamp. Since the calculation included neither the operation nor the request body, redirecting the call to another operation did not invalidate its authentication.

Conceptual HMAC input. Key K and timestamp t produce the tag. The RPC path and Protobuf body remain outside the authenticated data.
The MAC authenticates the timestamp without binding the operation or request body.

Same bytes, different meaning

The internal messages used Protobuf. In the binary representation, each field carries a number and a wire type, which tells the decoder how to read the value. For strings, for example, it reads the length first, then the bytes of the text.

The bytes do not include field or message names, so the decoder relies on the schema to interpret them. In the diagram’s toy example, field 7 is a string in both ToyA.text and ToyB.label. The same bytes for ready can therefore fill either field.

Illustrative schemas. Tag 0x3A encodes field 7 and wire type 2. Byte 0x05 gives the payload length. ToyA.text and ToyB.label both accept the same five UTF-8 bytes for ready.
Illustrative schemas. The tag carries the field number and wire type, not the field name.

In GHES, the GetSecrets response stored the key in a field with the same number and type as the error field in the CheckSystemRequirements response. Both accepted strings, so the decoder read the key as an error message.

Compatibility between the schemas explained why decoding succeeded despite receiving a response from the wrong operation.

PoC

I wrote a PoC script to automate the entire process, including submitting the cluster configuration, triggering SSRF through an internal redirect, extracting the Manage API key leaked in the Protobuf error field, and finally registering an SSH key under my control for the admin user. I reproduced this chain on GHES 3.21.3, 3.21.4, and 3.21.5.

Original recording of the PoC running against my local GHES server.

Timeline

Date Event
Aug 2, 2026 I sent the report to GitHub.
Aug 14, 2026 GitHub closed the report as a duplicate of an earlier case.
Sep 1, 2026 CVE-2026-18730, associated with the original report, was published.
Sep 26, 2026 I retested on 3.21.6 and confirmed that the chain no longer produced the earlier result.