🛡️ CVE-2026-49998 — centrifugo

🟠 CVSS 8.2 — High ✅ No Known Exploit CWE-347 NVD
8.2
CVSS Score
0 Low4 Medium7 High9 Critical10

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_endpoint
  • client.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 VerifyConnectToken from internal/client/handler.go:350-352.
  • Normal subscription tokens reach VerifySubscribeToken from internal/client/handler.go:769-775.
  • Subscription refresh tokens reach VerifySubscribeToken from internal/client/handler.go:628-632.

The verifier must parse token claims before signature verification to resolve the dynamic JWKS endpoint:

  • VerifyConnectToken parses without verification at internal/jwtverify/token_verifier_jwt.go:528-535, extracts template variables before signature verification at internal/jwtverify/token_verifier_jwt.go:539-548, then validates claims only after signature verification at internal/jwtverify/token_verifier_jwt.go:557-560.
  • VerifySubscribeToken follows the same pattern at internal/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

github.com (Web)
github.com (Package)

Details

Severity HIGH
CVSS Score 8.2
CVSS Vector CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:N
CWE CWE-347
Public Exploit ✅ No
Source NVD
Published 2026-07-01
Updated 2026-08-12
Modified 2026-07-07
Fix URL N/A

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

Similar Threats

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.