VYPR
Critical severityNVD Advisory· Published Nov 6, 2020· Updated Aug 4, 2024

CVE-2020-26892

CVE-2020-26892

Description

The JWT library in NATS nats-server before 2.1.9 has Incorrect Access Control because of how expired credentials are handled.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

NATS nats-server before 2.1.9 incorrectly handles expired JWT credentials, allowing unauthorized access after revocation.

Vulnerability

Description The NATS JWT library, used by nats-server before version 2.1.9, contains an incorrect access control flaw in how expired credentials are handled. Specifically, the server's revocation checks for user accounts and activation tokens were comparing revocation timestamps against the current system time (time.Now().Unix()) rather than the token's issuedAt time [3]. This meant that if a token was revoked, the revocation would only become effective after the current system time passed the revocation timestamp, but the token's expiration was not properly validated against the revocation. The fix introduced a new isRevoked function that correctly compares revocation timestamps to the token's issuedAt field [3].

Exploitation

Conditions This vulnerability can be exploited by any user possessing a previously valid JWT credential that has been revoked or whose expiration period has passed. The attack does not require authentication to the NATS server beyond presenting the expired or revoked JWT. The attacker must send a connection request using the stale token; due to the flawed logic, the server would accept it as valid because the revocation check was not properly tied to the token's issuance time [3]. The attack can be carried out remotely over the network.

Impact

Successful exploitation allows an attacker to gain unauthorized access to the NATS messaging system using credentials that should no longer be valid. The attacker could then publish or subscribe to subjects according to the permissions originally granted by the revoked or expired token, potentially leading to disclosure of sensitive messages or disruption of message flows. This is a direct violation of the access control intended by JWT revocation mechanisms.

Mitigation

NATS nats-server version 2.1.9, released 2020-11-02, contains the fix for this vulnerability [1][2][4]. Users must upgrade to this version or later to ensure that expired and revoked credentials are properly rejected. There are no known workarounds that address the root cause. The fix also covers the related nil dereference issue (CVE-2020-26521) that could cause a denial of service [4].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/nats-io/jwtGo
< 1.1.01.1.0

Affected products

6

Patches

2
1e08b67f08e1

[FIXED] User and claims activation revocation checks

https://github.com/nats-io/nats-serverIvan KozlovicOct 21, 2020via ghsa
2 files changed · +25 26
  • server/accounts.go+24 25 modified
    @@ -1525,6 +1525,16 @@ func (a *Account) activationExpired(exportAcc *Account, subject string, kind jwt
     	}
     }
     
    +func isRevoked(revocations map[string]int64, subject string, issuedAt int64) bool {
    +	if revocations == nil {
    +		return false
    +	}
    +	if t, ok := revocations[subject]; !ok || t < issuedAt {
    +		return false
    +	}
    +	return true
    +}
    +
     // checkActivation will check the activation token for validity.
     func (a *Account) checkActivation(importAcc *Account, claim *jwt.Import, expTimer bool) bool {
     	if claim == nil || claim.Token == "" {
    @@ -1567,13 +1577,7 @@ func (a *Account) checkActivation(importAcc *Account, claim *jwt.Import, expTime
     		}
     	}
     	// Check for token revocation..
    -	if a.actsRevoked != nil {
    -		if t, ok := a.actsRevoked[act.Subject]; ok && t <= time.Now().Unix() {
    -			return false
    -		}
    -	}
    -
    -	return true
    +	return !isRevoked(a.actsRevoked, act.Subject, act.IssuedAt)
     }
     
     // Returns true if the activation claim is trusted. That is the issuer matches
    @@ -1710,16 +1714,10 @@ func (a *Account) clearExpirationTimer() bool {
     }
     
     // checkUserRevoked will check if a user has been revoked.
    -func (a *Account) checkUserRevoked(nkey string) bool {
    +func (a *Account) checkUserRevoked(nkey string, issuedAt int64) bool {
     	a.mu.RLock()
     	defer a.mu.RUnlock()
    -	if a.usersRevoked == nil {
    -		return false
    -	}
    -	if t, ok := a.usersRevoked[nkey]; !ok || t > time.Now().Unix() {
    -		return false
    -	}
    -	return true
    +	return isRevoked(a.usersRevoked, nkey, issuedAt)
     }
     
     // Check expiration and set the proper state as needed.
    @@ -2010,6 +2008,8 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
     		for pk, t := range ac.Revocations {
     			a.usersRevoked[pk] = t
     		}
    +	} else {
    +		a.usersRevoked = nil
     	}
     	a.incomplete = len(incompleteImports) != 0
     	for _, i := range incompleteImports {
    @@ -2024,7 +2024,7 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
     			return clients[i].start.After(clients[j].start)
     		})
     	}
    -	now := time.Now().Unix()
    +
     	for i, c := range clients {
     		a.mu.RLock()
     		exceeded := a.mconns != jwt.NoLimit && i >= int(a.mconns)
    @@ -2035,17 +2035,16 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
     		}
     		c.mu.Lock()
     		c.applyAccountLimits()
    -		// Check for being revoked here. We use ac one to avoid
    -		// the account lock.
    -		var nkey string
    -		if c.user != nil {
    -			nkey = c.user.Nkey
    -		}
    +		theJWT := c.opts.JWT
     		c.mu.Unlock()
     
    -		// Check if we have been revoked.
    -		if ac.Revocations != nil {
    -			if t, ok := ac.Revocations[nkey]; ok && now >= t {
    +		// Check for being revoked here. We use ac one to avoid the account lock.
    +		if ac.Revocations != nil && theJWT != "" {
    +			if juc, err := jwt.DecodeUserClaims(theJWT); err != nil {
    +				c.Debugf("User JWT not valid: %v", err)
    +				c.authViolation()
    +				continue
    +			} else if ok := ac.IsClaimRevoked(juc); ok {
     				c.sendErrAndDebug("User Authentication Revoked")
     				c.closeConnection(Revocation)
     				continue
    
  • server/auth.go+1 1 modified
    @@ -474,7 +474,7 @@ func (s *Server) processClientOrLeafAuthentication(c *client) bool {
     			c.Debugf("Signature not verified")
     			return false
     		}
    -		if acc.checkUserRevoked(juc.Subject) {
    +		if acc.checkUserRevoked(juc.Subject, juc.IssuedAt) {
     			c.Debugf("User authentication revoked")
     			return false
     		}
    
e11ce317263c

Fixed documentation issue for jwt activation revocation (#106)

https://github.com/nats-io/jwtMatthias HanelOct 15, 2020via ghsa
3 files changed · +40 23
  • exports.go+6 6 modified
    @@ -151,16 +151,16 @@ func (e *Export) ClearRevocation(pubKey string) {
     	e.Revocations.ClearRevocation(pubKey)
     }
     
    -// IsRevokedAt checks if the public key is in the revoked list with a timestamp later than
    -// the one passed in. Generally this method is called with time.Now() but other time's can
    -// be used for testing.
    +// IsRevokedAt checks if the public key is in the revoked list with a timestamp later than the one passed in.
    +// Generally this method is called with the subject and issue time of the jwt to be tested.
    +// DO NOT pass time.Now(), it will not produce a stable/expected response.
     func (e *Export) IsRevokedAt(pubKey string, timestamp time.Time) bool {
     	return e.Revocations.IsRevoked(pubKey, timestamp)
     }
     
    -// IsRevoked checks if the public key is in the revoked list with time.Now()
    -func (e *Export) IsRevoked(pubKey string) bool {
    -	return e.Revocations.IsRevoked(pubKey, time.Now())
    +// IsRevoked does not perform a valid check. Use IsRevokedAt instead.
    +func (e *Export) IsRevoked(_ string) bool {
    +	return true
     }
     
     // Exports is a slice of exports
    
  • v2/exports.go+12 8 modified
    @@ -175,16 +175,20 @@ func (e *Export) ClearRevocation(pubKey string) {
     	e.Revocations.ClearRevocation(pubKey)
     }
     
    -// IsRevokedAt checks if the public key is in the revoked list with a timestamp later than
    -// the one passed in. Generally this method is called with time.Now() but other time's can
    -// be used for testing.
    -func (e *Export) IsRevokedAt(pubKey string, timestamp time.Time) bool {
    -	return e.Revocations.IsRevoked(pubKey, timestamp)
    +// isRevoked checks if the public key is in the revoked list with a timestamp later than the one passed in.
    +// Generally this method is called with the subject and issue time of the jwt to be tested.
    +// DO NOT pass time.Now(), it will not produce a stable/expected response.
    +func (e *Export) isRevoked(pubKey string, claimIssuedAt time.Time) bool {
    +	return e.Revocations.IsRevoked(pubKey, claimIssuedAt)
     }
     
    -// IsRevoked checks if the public key is in the revoked list with time.Now()
    -func (e *Export) IsRevoked(pubKey string) bool {
    -	return e.Revocations.IsRevoked(pubKey, time.Now())
    +// IsClaimRevoked checks if the activation revoked the claim passed in.
    +// Invalid claims (nil, no Subject or IssuedAt) will return true.
    +func (e *Export) IsClaimRevoked(claim *ActivationClaims) bool {
    +	if claim == nil || claim.IssuedAt == 0 || claim.Subject == "" {
    +		return true
    +	}
    +	return e.isRevoked(claim.Subject, time.Unix(claim.IssuedAt, 0))
     }
     
     // Exports is a slice of exports
    
  • v2/exports_test.go+22 9 modified
    @@ -187,48 +187,61 @@ func TestExportRevocation(t *testing.T) {
     
     	account.Exports.Add(e)
     
    -	pubKey := "bar"
    +	ikp := createAccountNKey(t)
    +	pubKey := publicKey(ikp, t)
    +
    +	ac := NewActivationClaims(pubKey)
    +	ac.IssuerAccount = apk
    +	ac.Name = "foo"
    +	ac.Activation.ImportSubject = "foo"
    +	ac.Activation.ImportType = Stream
    +	aJwt, _ := ac.Encode(akp)
    +	ac, err := DecodeActivationClaims(aJwt)
    +	if err != nil {
    +		t.Errorf("Failed to decode activation claim: %v", err)
    +	}
    +
     	now := time.Now()
     
     	// test that clear is safe before we add any
     	e.ClearRevocation(pubKey)
     
    -	if e.IsRevokedAt(pubKey, now) {
    +	if e.isRevoked(pubKey, now) {
     		t.Errorf("no revocation was added so is revoked should be false")
     	}
     
     	e.RevokeAt(pubKey, now.Add(time.Second*100))
     
    -	if !e.IsRevokedAt(pubKey, now) {
    +	if !e.isRevoked(pubKey, now) {
     		t.Errorf("revocation should hold when timestamp is in the future")
     	}
     
    -	if e.IsRevokedAt(pubKey, now.Add(time.Second*150)) {
    +	if e.isRevoked(pubKey, now.Add(time.Second*150)) {
     		t.Errorf("revocation should time out")
     	}
     
     	e.RevokeAt(pubKey, now.Add(time.Second*50)) // shouldn't change the revocation, you can't move it in
     
    -	if !e.IsRevokedAt(pubKey, now.Add(time.Second*60)) {
    +	if !e.isRevoked(pubKey, now.Add(time.Second*60)) {
     		t.Errorf("revocation should hold, 100 > 50")
     	}
     
     	encoded, _ := account.Encode(akp)
     	decoded, _ := DecodeAccountClaims(encoded)
     
    -	if !decoded.Exports[0].IsRevokedAt(pubKey, now.Add(time.Second*60)) {
    +	if !decoded.Exports[0].isRevoked(pubKey, now.Add(time.Second*60)) {
     		t.Errorf("revocation should last across encoding")
     	}
     
     	e.ClearRevocation(pubKey)
     
    -	if e.IsRevokedAt(pubKey, now) {
    +	if e.IsClaimRevoked(ac) {
     		t.Errorf("revocations should be cleared")
     	}
     
    -	e.RevokeAt(pubKey, now.Add(time.Second*1000))
    +	e.RevokeAt(pubKey, now)
     
    -	if !e.IsRevoked(pubKey) {
    +	if !e.IsClaimRevoked(ac) {
     		t.Errorf("revocation be true we revoked in the future")
     	}
     }
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

11

News mentions

0

No linked articles in our index yet.