GHSA-j77h-rr39-c552 Centrifugo: SSRF via unverified JWT claims interpolated into dynamic JWKS endpoint URL
SummaryCentrifugo is vulnerable to Server-Side Request Forgery (SSRF) when configured with a dynamic JWKS endpoint URL using template variables (e.g. `{{tenant}}`). An unauthenticated attacker can craft a JWT with a malicious `iss` or `aud` claim value that gets interpolated into the JWKS fetch URL **before the token signature is verified**, causing Centrifugo to make an outbound HTTP request to an attacker-controlled destination.DetailsIn `internal/jwtverify/token_verifier_jwt.go`, the functions `VerifyConnectToken` and `VerifySubscribeToken` follow this flawed order of operations:Token is parsed without verification: `jwt.ParseNoVerify([]byte(t))`Claims are decoded from the unverified token`validateClaims()` runs — extracting named regex capture groups from 
`issuer_regex`/`audience_regex` into `tokenVars` map using attacker-controlled 
`iss`/`aud` claim values`verifySignatureByJWK(token, tokenVars)` is called — passing attacker-controlled 
`tokenVars` to the JWKS managerIn `internal/jwks/manager.go`, `fetchKey()` interpolates `tokenVars` directly 
into the JWKS URL:
`jwkURL := m.url.ExecuteString(tokenVars)`Centrifugo makes an HTTP GET request to the attacker-controlled URLSuppressed the security linter on this line with an incorrect comment:
`//nolint:gosec // URL is from server configuration, not user input.`
The URL is NOT purely from server configuration — it is partially constructed from unverified user-supplied JWT claims.Signature verification happens too late — after the SSRF has already fired.PoC**Required config** (`config.json`):**Step 1** — Start listener on attacker machine:**Step 2** — Generate malicious unsigned JWT:**Step 3** — Connect to Centrifugo WebSocket with the malicious token:**Step 4** — Observe incoming HTTP request on attacker listener:Malicious token being crafted with suppress_origin=True bypassing the 403, and the token sent to Centrifugo:
![1](https://github.com/user-attachments/assets/6fd5d5b8-f47a-4899-94db-931f52504808)Centrifugo Server Log:
![2](https://github.com/user-attachments/assets/2e802648-8dc9-40d7-ac9e-f5f2ca19acad)netcat terminal:
![3](https://github.com/user-attachments/assets/854cfb19-ed0c-44e2-977a-efe2f9b6c50a)Impact**Unauthenticated SSRF** — No valid credentials requiredAttacker can probe and access internal network services not exposed externallyOn cloud deployments: access to metadata endpoints (AWS: `169.254.169.254`, GCP: `metadata.google.internal`) to steal IAM credentialsAttacker can serve a malicious JWKS response containing their own public key, causing Centrifugo to accept attacker-signed tokens as legitimate — leading to **full authentication bypass**Exploitation requires `jwks_public_endpoint` to contain `{{...}}` template variables combined with `issuer_regex` or `audience_regex` — a configuration pattern explicitly documented and promoted by CentrifugoSuggested Fix**1. Verify signature BEFORE extracting tokenVars (critical fix):**
In `token_verifier_jwt.go`, swap the order of operations:**2. Fix the incorrect nolint comment in `manager.go`:**
Remove `//nolint:gosec // URL is from server configuration, not user input` The URL IS partially constructed from user input via JWT claims.**3. Alternative mitigation:**
Restrict template variables to only the `kid` header field (which is not claim data) rather than allowing arbitrary claim values to influence the JWKS URL.
CVSS v3.1 score: 9.3