# Centrifugo: Client-forgeable headers emulation lets any client spoof headers forwarded to proxy backends

Centrifugo forwards client-supplied `headers` map from the connect command as emulated headers to proxy backends, letting any client spoof trusted identity headers.

- Payout: Research
- Severity: critical
- Program: Centrifugo
- Found by: iaohkut-from-NightWolf-Team
- Disclosed: 2026-09-02
- CVE: CVE-2026-71485
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-9468-v6mj-fppw
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-9468-v6mj-fppw
- Tags: area:web domain:api impact:integrity impact:privesc kind:research platform:ghsa sev:critical tech:go tech:grpc tech:websocket technique:source-review vuln:access-control vuln:auth-bypass

**Target:** Centrifugo proxy backend header forwarding (WS and uniGRPC transports)
**Root cause:** The `headers` field in `protocol.ConnectRequest` is populated from the client's own connect message, not from real transport-level HTTP headers, yet it is copied into `ConnectEvent.Headers` and forwarded to proxy backends for any name in the `http_headers`/`grpc_metadata` allowlist. Real HTTP headers only override when the same name is also present on the genuine request; uniGRPC has no HTTP request at all.
**Steps:**
1. Configure a connect proxy with `http_headers: ["x-trusted-user"]`.
2. Connect to Centrifugo over WebSocket and send `{"connect": {"headers": {"x-trusted-user": "admin-impersonation-via-headers-emulation"}}}`.
3. Observe the backend receives `X-Trusted-User: admin-impersonation-via-headers-emulation` despite no such header on the upgrade request.
4. For uniGRPC, call `Consume` with `ConnectRequest(headers={"x-trusted-user": "admin-impersonation-via-unigrpc"})` and zero real gRPC metadata; backend receives the spoofed header.
**How found:** Code review of `requestHeaders`/`requestMetadata` revealed the emulated-headers context value is sourced from `ConnectEvent.Headers`, which traces back to the client-supplied `protocol.ConnectRequest.headers` field rather than transport-level HTTP headers; the uniGRPC path has no real-header fallback at all. Confirmed dynamically against the official v6.8.3 Docker image on both WS and uniGRPC with a logging backend and control tests.
**Exploitation:** Attacker connects directly to Centrifugo (no trusted intermediary needed) and supplies arbitrary values for any header name in the operator's allowlist via the connect command's `headers` map. Centrifugo forwards them to the proxy backend for every proxy call type (connect, refresh, subscribe, publish, rpc, etc.) for the connection lifetime, so a backend trusting that header for identity/authorization grants the attacker's chosen identity. No credentials, JWT, or API key required.
**Escalation:** Spoofed header persists for the entire connection lifetime and applies to all proxy call types, enabling full account/identity impersonation depending on backend logic.
**Insight:** When a proxy/gateway feature is documented as forwarding 'incoming HTTP headers', verify the actual source of those headers in the wire protocol — a `headers` map inside the client's own connect message is not the same as transport-level headers, and transports without an HTTP layer (uniGRPC) have no unforgeable fallback.

## Proof of concept (copied exactly from the original report)

**WebSocket connect command spoofing trusted header**

```
{"connect": {"headers": {"x-trusted-user": "admin-impersonation-via-headers-emulation"}}}` resulted in the backend receiving `X-Trusted-User: admin-impersonation-via-headers-emulation
```

**uniGRPC Consume RPC with forged ConnectRequest headers**

```
ConnectRequest(headers={"x-trusted-user": "admin-impersonation-via-unigrpc"})` (zero real gRPC metadata set) resulted in the backend receiving `X-Trusted-User: admin-impersonation-via-unigrpc
```

