contents [06]
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.

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.

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.

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.
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. |
