🛡️ CVE-2026-49998 — centrifugo
Description
Centrifugo's dynamic JWKS key cache keyed only by kid allows cross-issuer JWT authentication bypass
Summary
Centrifugo's dynamic JWKS endpoint feature can verify a JWT for one allowed issuer using a public key cached from another allowed issuer. The JWKS cache and singleflight lookup are keyed only by the JWT header kid, not by the resolved JWKS endpoint, issuer, audience, or other trust-domain namespace.
In a documented multi-issuer dynamic JWKS configuration, an attacker who can obtain or mint a valid token for issuer/tenant A can authenticate as issuer/tenant B if both JWKS documents use the same kid value and tenant A's key is cached first. This affects connection token verification and subscription token verification because both paths use the same JWKS verification manager.
Details
The vulnerable path is reachable when either of these shipped configuration options is set to a templated JWKS URL using values derived from JWT iss or aud claims:
client.token.jwks_public_endpointclient.subscription_token.jwks_public_endpoint
Relevant shipped config fields are defined in internal/configtypes/types.go:59-65, mapped into verifier configuration in internal/confighelpers/jwt.go:36-41, and exposed in the generated config schema at internal/cli/configdoc/schema.json:3927, 3947, 3967, 3987, 4069, 4089, 4109, and 4129. Dynamic JWKS endpoints based on iss and aud are documented in the project changelog at CHANGELOG.md:107.
External clients control JWT connection and subscription tokens:
- Connection tokens reach
VerifyConnectTokenfrominternal/client/handler.go:350-352. - Normal subscription tokens reach
VerifySubscribeTokenfrominternal/client/handler.go:769-775. - Subscription refresh tokens reach
VerifySubscribeTokenfrominternal/client/handler.go:628-632.
The verifier must parse token claims before signature verification to resolve the dynamic JWKS endpoint:
VerifyConnectTokenparses without verification atinternal/jwtverify/token_verifier_jwt.go:528-535, extracts template variables before signature verification atinternal/jwtverify/token_verifier_jwt.go:539-548, then validates claims only after signature verification atinternal/jwtverify/token_verifier_jwt.go:557-560.VerifySubscribeTokenfollows the same pattern atinternal/jwtverify/token_verifier_jwt.go:700-732.
The problem is that the JWKS cache lookup ignores the endpoint/trust domain selected by those token variables. internal/jwtverify/token_verifier_jwt.go:242-245 passes only the JWT header kid plus token-derived variables to the JWKS manager:
```go
func (j *jwksManager) verify(token *jwt.Token, tokenVars map[string]any) error {
kid := token.Header().KeyID
key, err := j.Manager.FetchKey(context.Background(), kid, tokenVars)
```
internal/jwks/manager.go:96-117 checks cache and singleflight using only kid:
```go
func (m *Manager) FetchKey(ctx context.Context, kid string, tokenVars map[string]any) (*JWK, error) {
if kid == "" {
return nil, ErrKeyIDNotProvided
}
if m.useCache {
key, err := m.cache.Get(kid)
if err == nil {
return key, nil
}
}
v, err, _ := m.group.Do(kid, func() (any, error) {
return m.fetchKey(ctx, kid, tokenVars)
})
```
The resolved JWKS URL is computed only later in internal/jwks/manager.go:133-149:
```go
func (m *Manager) fetchKey(ctx context.Context, kid string, tokenVars map[string]any) (*JWK, error) {
jwkURL := m.url.ExecuteString(tokenVars)
...
req, err := http.NewRequestWithContext(ctx, http.MethodGet, jwkURL, nil)
```
The TTL cache also stores and retrieves keys only by kid at internal/jwks/cache_ttl.go:82-101:
```go
func (tc *TTLCache) Add(key *JWK) error {
...
tc.items[key.Kid] = item
}
func (tc *TTLCache) Get(kid string) (*JWK, error) {
...
item, ok := tc.items[kid]
```
As a result, a key fetched from tenant A's JWKS endpoint can be reused to verify a token claiming tenant B before tenant B's JWKS endpoint is consulted.
I also reviewed the template safety mitigation in internal/jwtverify/validate.go:99-154. It restricts placeholder regex groups to finite literal alternatives, which helps prevent arbitrary endpoint substitution, but it does not scope cached keys by the resolved endpoint or issuer/audience namespace. The PoC uses a validator-accepted issuer regex: ^(?P<tenant>tenant-a|tenant-b)$.
PoC
This is a safe local-only unit test using httptest.Server and generated RSA key pairs. It does not contact external systems.
From a clean checkout of centrifugal/centrifugo at commit 458ee0500f046877d7e8375e32f5e842bc95535b, add this file as internal/jwtverify/jwks_cache_poc_test.go:
```go
package jwtverify
import (
"crypto/rsa"
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"github.com/centrifugal/centrifugo/v
How this vulnerability can be exploited
This issue can be reached over the network, attack complexity is high, an attacker needs low-level privileges on the target. No user interaction is required. The scope is changed, meaning a successful attack can affect components beyond the vulnerable one. Rated impact: confidentiality high, integrity high, availability none.
Weakness class
CVE-2026-49998 is classified as CWE-347: Improper Verification of Cryptographic Signature. A signature is not checked correctly, so forged or modified content is accepted as genuine.
Affected software
CVE-2026-49998 is recorded against 6 packages.
- github.com/centrifugal/centrifugo
- github.com/centrifugal/centrifugo/v3
- github.com/centrifugal/centrifugo/v4
- github.com/centrifugal/centrifugo/v5
- github.com/centrifugal/centrifugo/v6
- unknown
Timeline and source
Published on 1 July 2026 and last revised on 7 July 2026. No public exploit is currently recorded for this entry. Record sourced from NVD.
References
Details
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:N
Affected Packages
| Software | From version | Fixed in |
|---|---|---|
| github.com/centrifugal/centrifugo | — | — |
| github.com/centrifugal/centrifugo/v3 | — | — |
| github.com/centrifugal/centrifugo/v4 | — | — |
| github.com/centrifugal/centrifugo/v5 | — | — |
| github.com/centrifugal/centrifugo/v6 | — | — |
| unknown | — | — |
References
Similar Threats
- Unknown GO-2026-4703
- Critical CVE-2026-32301
- Low GHSA-q926-c743-49qj
- Unknown GO-2026-4528
- Unknown GHSA-j9wf-6r2x-hqmx
More CVE 2026 advisories
Browse all of CVE 2026 in the advisory index.
Site Security Check
Is centrifugo part of your stack?
CVE-2026-49998 is rated CVSS 8.2 High. BotEraser scans your installation against known CVE records and tells you whether this vulnerability applies to the versions you actually run.
Scan My Site Free →No credit card required · Results in minutes
ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.