CVE-2020-26521
Description
The JWT library in NATS nats-server before 2.1.9 allows a denial of service (a nil dereference in Go code).
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
NATS nats-server before 2.1.9 contains a nil pointer dereference in JWT handling, allowing a denial-of-service panic.
Vulnerability
Description
The JWT library in NATS nats-server prior to version 2.1.9 suffers from a nil pointer dereference vulnerability in Go code. The flaw resides in the validation of JWT tokens within the jwt package, specifically when processing crafted tokens that trigger a nil pointer access during import/export validation [1][3]. This leads to a runtime panic, causing a denial-of-service condition.
Exploitation
An attacker can exploit this vulnerability by sending a maliciously crafted JWT to the NATS server. No special authentication or network position is required beyond the ability to connect to the server, as the panic occurs during the validation of the token before or during the authentication process [2][4]. The attacker does not need to be an authenticated user, as the trigger happens at the token parsing stage.
Impact
Successful exploitation causes the nats-server to crash due to an unhandled nil dereference, resulting in a denial of service. This can disrupt messaging operations for all clients connected to the affected server, making the service unavailable until restarted [3].
Mitigation
The vulnerability is fixed in NATS nats-server version 2.1.9. Users are strongly advised to upgrade to this version or later. The fix was implemented in commit 9ff8bcde2e46, which introduces proper nil checks before pointer dereference during JWT processing [4]. No workarounds are documented; upgrading is the recommended mitigation.
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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/nats-io/jwtGo | < 1.1.0 | 1.1.0 |
Affected products
6- NATS/nats-serverdescription
- osv-coords5 versionspkg:apk/chainguard/nats-serverpkg:apk/chainguard/nats-server-compatpkg:apk/wolfi/nats-serverpkg:apk/wolfi/nats-server-compatpkg:golang/github.com/nats-io/jwt
< 0+ 4 more
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 1.1.0
Patches
19ff8bcde2e46[FIXED] Possible panic if server receives a maliciously crafted JWT
19 files changed · +112 −51
go.mod+1 −1 modified@@ -1,7 +1,7 @@ module github.com/nats-io/nats-server/v2 require ( - github.com/nats-io/jwt v0.3.2 + github.com/nats-io/jwt v1.1.0 github.com/nats-io/nats.go v1.10.0 github.com/nats-io/nkeys v0.1.4 github.com/nats-io/nuid v1.0.1
go.sum+2 −0 modified@@ -10,6 +10,8 @@ github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/jwt v1.1.0 h1:+vOlgtM0ZsF46GbmUoadq0/2rChNS45gtxHEa3H1gqM= +github.com/nats-io/jwt v1.1.0/go.mod h1:n3cvmLfBfnpV4JJRN7lRYCyZnw48ksGsbThGXEk4w9M= github.com/nats-io/nats.go v1.10.0 h1:L8qnKaofSfNFbXg0C5F71LdjPRnmQwSsA4ukmkt1TvY= github.com/nats-io/nats.go v1.10.0/go.mod h1:AjGArbfyR50+afOUotNX2Xs5SYHf+CoOa5HH1eEl2HE= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
server/accounts.go+3 −3 modified@@ -1546,14 +1546,14 @@ func (a *Account) checkActivation(importAcc *Account, claim *jwt.Import, expTime if err != nil { return false } + if !a.isIssuerClaimTrusted(act) { + return false + } vr = jwt.CreateValidationResults() act.Validate(vr) if vr.IsBlocking(true) { return false } - if !a.isIssuerClaimTrusted(act) { - return false - } if act.Expires != 0 { tn := time.Now().Unix() if act.Expires <= tn {
server/jwt_test.go+5 −3 modified@@ -1575,13 +1575,14 @@ func TestAccountURLResolver(t *testing.T) { defer ts.Close() confTemplate := ` + operator: %s listen: -1 resolver: URL("%s/ngs/v1/accounts/jwt/") resolver_tls { insecure: true } ` - conf := createConfFile(t, []byte(fmt.Sprintf(confTemplate, ts.URL))) + conf := createConfFile(t, []byte(fmt.Sprintf(confTemplate, ojwt, ts.URL))) defer os.Remove(conf) s, opts := RunServerWithConfig(conf) @@ -1660,10 +1661,11 @@ func TestAccountURLResolverNoFetchOnReload(t *testing.T) { defer ts.Close() confTemplate := ` + operator: %s listen: -1 resolver: URL("%s/ngs/v1/accounts/jwt/") ` - conf := createConfFile(t, []byte(fmt.Sprintf(confTemplate, ts.URL))) + conf := createConfFile(t, []byte(fmt.Sprintf(confTemplate, ojwt, ts.URL))) defer os.Remove(conf) s, _ := RunServerWithConfig(conf) @@ -1685,7 +1687,7 @@ func TestAccountURLResolverNoFetchOnReload(t *testing.T) { })) defer ts.Close() - changeCurrentConfigContentWithNewContent(t, conf, []byte(fmt.Sprintf(confTemplate, ts.URL))) + changeCurrentConfigContentWithNewContent(t, conf, []byte(fmt.Sprintf(confTemplate, ojwt, ts.URL))) if err := s.Reload(); err != nil { t.Fatalf("Error on reload: %v", err)
server/server.go+3 −0 modified@@ -1083,6 +1083,9 @@ func (s *Server) verifyAccountClaims(claimJWT string) (*jwt.AccountClaims, strin if err != nil { return nil, _EMPTY_, err } + if !s.isTrustedIssuer(accClaims.Issuer) { + return nil, _EMPTY_, ErrAccountValidation + } vr := jwt.CreateValidationResults() accClaims.Validate(vr) if vr.IsBlocking(true) {
test/operator_test.go+28 −14 modified@@ -155,7 +155,15 @@ func runOperatorServer(t *testing.T) (*server.Server, *server.Options) { return RunServerWithConfig(testOpConfig) } -func createAccountForOperatorKey(t *testing.T, s *server.Server, seed []byte) (*server.Account, nkeys.KeyPair) { +func publicKeyFromKeyPair(t *testing.T, pair nkeys.KeyPair) (pkey string) { + var err error + if pkey, err = pair.PublicKey(); err != nil { + t.Fatalf("Expected no error %v", err) + } + return +} + +func createAccountForOperatorKey(t *testing.T, s *server.Server, seed []byte) nkeys.KeyPair { t.Helper() okp, _ := nkeys.FromSeed(seed) akp, _ := nkeys.CreateAccount() @@ -165,16 +173,18 @@ func createAccountForOperatorKey(t *testing.T, s *server.Server, seed []byte) (* if err := s.AccountResolver().Store(pub, jwt); err != nil { t.Fatalf("Account Resolver returned an error: %v", err) } - acc, err := s.LookupAccount(pub) - if err != nil { - t.Fatalf("Error looking up account: %v", err) - } - return acc, akp + return akp } -func createAccount(t *testing.T, s *server.Server) (*server.Account, nkeys.KeyPair) { +func createAccount(t *testing.T, s *server.Server) (acc *server.Account, akp nkeys.KeyPair) { t.Helper() - return createAccountForOperatorKey(t, s, oSeed) + akp = createAccountForOperatorKey(t, s, oSeed) + if pub, err := akp.PublicKey(); err != nil { + t.Fatalf("Expected this to pass, got %v", err) + } else if acc, err = s.LookupAccount(pub); err != nil { + t.Fatalf("Error looking up account: %v", err) + } + return acc, akp } func createUserCreds(t *testing.T, s *server.Server, akp nkeys.KeyPair) nats.Option { @@ -215,11 +225,15 @@ func TestOperatorServer(t *testing.T) { // Now create an account from another operator, this should fail. okp, _ := nkeys.CreateOperator() seed, _ := okp.Seed() - _, akp = createAccountForOperatorKey(t, s, seed) + akp = createAccountForOperatorKey(t, s, seed) _, err = nats.Connect(url, createUserCreds(t, s, akp)) if err == nil { t.Fatalf("Expected error on connect") } + // The account should not be in memory either + if v, err := s.LookupAccount(publicKeyFromKeyPair(t, akp)); err == nil { + t.Fatalf("Expected account to NOT be in memory: %v", v) + } } func TestOperatorSystemAccount(t *testing.T) { @@ -229,15 +243,15 @@ func TestOperatorSystemAccount(t *testing.T) { // Create an account from another operator, this should fail if used as a system account. okp, _ := nkeys.CreateOperator() seed, _ := okp.Seed() - acc, _ := createAccountForOperatorKey(t, s, seed) - if err := s.SetSystemAccount(acc.Name); err == nil { + akp := createAccountForOperatorKey(t, s, seed) + if err := s.SetSystemAccount(publicKeyFromKeyPair(t, akp)); err == nil { t.Fatalf("Expected this to fail") } if acc := s.SystemAccount(); acc != nil { t.Fatalf("Expected no account to be set for system account") } - acc, _ = createAccount(t, s) + acc, _ := createAccount(t, s) if err := s.SetSystemAccount(acc.Name); err != nil { t.Fatalf("Expected this succeed, got %v", err) } @@ -251,10 +265,10 @@ func TestOperatorSigningKeys(t *testing.T) { defer s.Shutdown() // Create an account with a signing key, not the master key. - acc, akp := createAccountForOperatorKey(t, s, skSeed) + akp := createAccountForOperatorKey(t, s, skSeed) // Make sure we can set system account. - if err := s.SetSystemAccount(acc.Name); err != nil { + if err := s.SetSystemAccount(publicKeyFromKeyPair(t, akp)); err != nil { t.Fatalf("Expected this succeed, got %v", err) }
vendor/github.com/nats-io/jwt/account_claims.go+17 −7 modified@@ -194,7 +194,8 @@ func (a *AccountClaims) Revoke(pubKey string) { a.RevokeAt(pubKey, time.Now()) } -// RevokeAt enters a revocation by publickey and timestamp into this export +// RevokeAt enters a revocation by public key and timestamp into this account +// This will revoke all jwt issued for pubKey, prior to timestamp // If there is already a revocation for this public key that is newer, it is kept. func (a *AccountClaims) RevokeAt(pubKey string, timestamp time.Time) { if a.Revocations == nil { @@ -209,14 +210,23 @@ func (a *AccountClaims) ClearRevocation(pubKey string) { a.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 (a *AccountClaims) IsRevokedAt(pubKey string, timestamp time.Time) bool { return a.Revocations.IsRevoked(pubKey, timestamp) } -// IsRevoked checks if the public key is in the revoked list with time.Now() -func (a *AccountClaims) IsRevoked(pubKey string) bool { - return a.Revocations.IsRevoked(pubKey, time.Now()) +// IsRevoked does not perform a valid check. Use IsRevokedAt instead. +func (a *AccountClaims) IsRevoked(_ string) bool { + return true +} + +// IsClaimRevoked checks if the account revoked the claim passed in. +// Invalid claims (nil, no Subject or IssuedAt) will return true. +func (a *AccountClaims) IsClaimRevoked(claim *UserClaims) bool { + if claim == nil || claim.IssuedAt == 0 || claim.Subject == "" { + return true + } + return a.Revocations.IsRevoked(claim.Subject, time.Unix(claim.IssuedAt, 0)) }
vendor/github.com/nats-io/jwt/creds_utils.go+1 −1 modified@@ -82,7 +82,7 @@ NKEYs are sensitive and should be treated as secrets. return w.Bytes(), nil } -var userConfigRE = regexp.MustCompile(`\s*(?:(?:[-]{3,}[^\n]*[-]{3,}\n)(.+)(?:\n\s*[-]{3,}[^\n]*[-]{3,}\n))`) +var userConfigRE = regexp.MustCompile(`\s*(?:(?:[-]{3,}.*[-]{3,}\r?\n)([\w\-.=]+)(?:\r?\n[-]{3,}.*[-]{3,}\r?\n))`) // An user config file looks like this: // -----BEGIN NATS USER JWT-----
vendor/github.com/nats-io/jwt/exports.go+14 −6 modified@@ -108,6 +108,10 @@ func (e *Export) IsStreamResponse() bool { // Validate appends validation issues to the passed in results list func (e *Export) Validate(vr *ValidationResults) { + if e == nil { + vr.AddError("null export is not allowed") + return + } if !e.IsService() && !e.IsStream() { vr.AddError("invalid export type: %q", e.Type) } @@ -146,16 +150,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 @@ -199,6 +203,10 @@ func (e *Exports) Validate(vr *ValidationResults) error { var streamSubjects []Subject for _, v := range *e { + if v == nil { + vr.AddError("null export is not allowed") + continue + } if v.IsService() { serviceSubjects = append(serviceSubjects, v.Subject) } else {
vendor/github.com/nats-io/jwt/go.mod+3 −1 modified@@ -1,3 +1,5 @@ module github.com/nats-io/jwt -require github.com/nats-io/nkeys v0.1.3 +require github.com/nats-io/nkeys v0.1.4 + +go 1.13
vendor/github.com/nats-io/jwt/go.sum+4 −4 modified@@ -1,8 +1,8 @@ -github.com/nats-io/nkeys v0.1.3 h1:6JrEfig+HzTH85yxzhSVbjHRJv9cn0p6n3IngIcM5/k= -github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.4 h1:aEsHIssIk6ETN5m2/MD8Y4B2X7FfXrBAUdkyRvbVYzA= +github.com/nats-io/nkeys v0.1.4/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
vendor/github.com/nats-io/jwt/header.go+1 −1 modified@@ -23,7 +23,7 @@ import ( const ( // Version is semantic version. - Version = "0.3.2" + Version = "1.1.0" // TokenTypeJwt is the JWT token type supported JWT tokens // encoded and decoded by this library
vendor/github.com/nats-io/jwt/imports.go+8 −0 modified@@ -53,6 +53,10 @@ func (i *Import) IsStream() bool { // Validate checks if an import is valid for the wrapping account func (i *Import) Validate(actPubKey string, vr *ValidationResults) { + if i == nil { + vr.AddError("null import is not allowed") + return + } if !i.IsService() && !i.IsStream() { vr.AddError("invalid import type: %q", i.Type) } @@ -123,6 +127,10 @@ type Imports []*Import func (i *Imports) Validate(acctPubKey string, vr *ValidationResults) { toSet := make(map[Subject]bool, len(*i)) for _, v := range *i { + if v == nil { + vr.AddError("null import is not allowed") + continue + } if v.Type == Service { if _, ok := toSet[v.To]; ok { vr.AddError("Duplicate To subjects for %q", v.To)
vendor/github.com/nats-io/jwt/operator_claims.go+8 −0 modified@@ -40,6 +40,8 @@ type Operator struct { // A list of NATS urls (tls://host:port) where tools can connect to the server // using proper credentials. OperatorServiceURLs StringList `json:"operator_service_urls,omitempty"` + // Identity of the system account + SystemAccount string `json:"system_account,omitempty"` } // Validate checks the validity of the operators contents @@ -63,6 +65,12 @@ func (o *Operator) Validate(vr *ValidationResults) { vr.AddError("%s is not an operator public key", k) } } + + if o.SystemAccount != "" { + if !nkeys.IsValidPublicAccountKey(o.SystemAccount) { + vr.AddError("%s is not an account public key", o.SystemAccount) + } + } } func (o *Operator) validateAccountServerURL() error {
vendor/github.com/nats-io/jwt/revocation_list.go+2 −2 modified@@ -24,9 +24,9 @@ func (r RevocationList) ClearRevocation(pubKey string) { } // 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 time.Now() but other time's can +// the one passed in. Generally this method is called with an issue time but other time's can // be used for testing. func (r RevocationList) IsRevoked(pubKey string, timestamp time.Time) bool { ts, ok := r[pubKey] - return ok && ts > timestamp.Unix() + return ok && ts >= timestamp.Unix() }
vendor/github.com/nats-io/jwt/.travis.yml+2 −3 modified@@ -1,8 +1,7 @@ language: go -sudo: false go: +- 1.14.x - 1.13.x -- 1.12.x install: - go get -t ./... @@ -19,4 +18,4 @@ before_script: script: - go test -v -race ./... -- if [[ "$TRAVIS_GO_VERSION" =~ 1.12 ]]; then ./scripts/cov.sh TRAVIS; fi +- if [[ "$TRAVIS_GO_VERSION" =~ 1.13 ]]; then ./scripts/cov.sh TRAVIS; fi
vendor/github.com/nats-io/jwt/types.go+0 −2 modified@@ -241,8 +241,6 @@ type Permissions struct { // Validate the pub and sub fields in the permissions list func (p *Permissions) Validate(vr *ValidationResults) { - p.Pub.Validate(vr) - p.Sub.Validate(vr) if p.Resp != nil { p.Resp.Validate(vr) }
vendor/github.com/nats-io/jwt/user_claims.go+7 −0 modified@@ -25,12 +25,14 @@ import ( type User struct { Permissions Limits + BearerToken bool `json:"bearer_token,omitempty"` } // Validate checks the permissions and limits in a User jwt func (u *User) Validate(vr *ValidationResults) { u.Permissions.Validate(vr) u.Limits.Validate(vr) + // When BearerToken is true server will ignore any nonce-signing verification } // UserClaims defines a user JWT @@ -97,3 +99,8 @@ func (u *UserClaims) Payload() interface{} { func (u *UserClaims) String() string { return u.ClaimsData.String(u) } + +// IsBearerToken returns true if nonce-signing requirements should be skipped +func (u *UserClaims) IsBearerToken() bool { + return u.BearerToken +}
vendor/modules.txt+3 −3 modified@@ -1,6 +1,4 @@ -# github.com/golang/protobuf v1.3.5 -## explicit -# github.com/nats-io/jwt v0.3.2 +# github.com/nats-io/jwt v1.1.0 ## explicit github.com/nats-io/jwt # github.com/nats-io/nats.go v1.10.0 @@ -27,3 +25,5 @@ golang.org/x/sys/windows/registry golang.org/x/sys/windows/svc golang.org/x/sys/windows/svc/eventlog golang.org/x/sys/windows/svc/mgr +# google.golang.org/protobuf v1.22.0 +## explicit
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
11- github.com/advisories/GHSA-h2fg-54x9-5qhqghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/VT67XCLIIBYRT762SVFBYFFTQFVSM3SI/mitrevendor-advisoryx_refsource_FEDORA
- nvd.nist.gov/vuln/detail/CVE-2020-26521ghsaADVISORY
- www.openwall.com/lists/oss-security/2020/11/02/2ghsax_refsource_CONFIRMWEB
- advisories.nats.io/CVE/CVE-2020-26521.txtghsaWEB
- github.com/nats-io/jwt/pull/107ghsaWEB
- github.com/nats-io/jwt/security/advisories/GHSA-h2fg-54x9-5qhqghsaWEB
- github.com/nats-io/nats-server/commit/9ff8bcde2e46009e98bd9e88f598af355f62c168ghsaWEB
- github.com/nats-io/nats-server/commits/masterghsax_refsource_MISCWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/VT67XCLIIBYRT762SVFBYFFTQFVSM3SIghsaWEB
- pkg.go.dev/vuln/GO-2022-0402ghsaWEB
News mentions
0No linked articles in our index yet.