CVE-2021-3127
Description
NATS Server 2.x before 2.2.0 and JWT library before 2.0.1 have Incorrect Access Control because Import Token bindings are mishandled.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2021-3127: NATS JWT library incorrectly warns on mismatched Import Token bindings instead of rejecting them, allowing any account to reuse another account's import token to gain unauthorized cross-account subject access.
CVE-2021-3127 is an access control vulnerability in the NATS server (versions 2.x before 2.2.0) and its JWT library (versions before 2.0.1). The root cause is that the JWT library's validation of Import Token bindings mistakenly only produced a warning on mismatches, rather than outright rejecting the token [1]. This allows an attacker to take an Import Token from one account and re-use it for a different account, effectively bypassing the intended account isolation [1].
Exploitation
The NATS account-server system treats account JWTs as semi-public information, so an attacker can easily enumerate all account JWTs and retrieve all Import Tokens from them [1]. By reusing an Import Token from another account, the attacker can import any Subject from the exporting account, not just the Subject originally referenced in the token [1]. The fix, implemented in pull request #149 [2] and commit 6c72fdd [3], changes the validation to return an error when the token context is wrong, making such tokens blocking instead of merely warning [3].
Impact
In deployments with untrusted accounts able to update their account JWT, a malicious account holder can import any subject from any other account's exports, regardless of whether the export was intended to be private [1]. This violates the core NATS security model that subjects should be private to an account unless explicitly exported and imported with proper authorization.
Mitigation
Users should upgrade to nats-server 2.2.0 or later, and nats-io/jwt 2.0.1 or later [1]. The test files show that the updated validation now correctly returns blocking errors for bad tokens [3]. No workaround is available; upgrading is the only remedy.
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.2.2 | — |
github.com/nats-io/jwt/v2Go | < 2.0.1 | 2.0.1 |
Affected products
8- NATS/NATS Serverdescription
- osv-coords7 versionspkg:apk/chainguard/nats-serverpkg:apk/chainguard/nats-server-compatpkg:apk/wolfi/nats-serverpkg:apk/wolfi/nats-server-compatpkg:bitnami/natspkg:golang/github.com/nats-io/jwtpkg:golang/github.com/nats-io/jwt/v2
< 0+ 6 more
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: < 0
- (no CPE)range: >= 2.0.0, < 2.2.0
- (no CPE)range: <= 1.2.2
- (no CPE)range: < 2.0.1
Patches
16c72fdd73e82Merge pull request #149 from nats-io/cve
6 files changed · +91 −93
activation_claims.go+8 −1 modified@@ -102,7 +102,14 @@ func (a *ActivationClaims) Payload() interface{} { // Validate checks the claims func (a *ActivationClaims) Validate(vr *ValidationResults) { - a.ClaimsData.Validate(vr) + a.validateWithTimeChecks(vr, true) +} + +// Validate checks the claims +func (a *ActivationClaims) validateWithTimeChecks(vr *ValidationResults, timeChecks bool) { + if timeChecks { + a.ClaimsData.Validate(vr) + } a.Activation.Validate(vr) if a.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(a.IssuerAccount) { vr.AddError("account_id is not an account public key")
imports.go+21 −14 modified@@ -62,7 +62,7 @@ func (i *Import) Validate(actPubKey string, vr *ValidationResults) { } if i.Account == "" { - vr.AddWarning("account to import from is not specified") + vr.AddError("account to import from is not specified") } i.Subject.Validate(vr) @@ -78,46 +78,53 @@ func (i *Import) Validate(actPubKey string, vr *ValidationResults) { if i.Token != "" { // Check to see if its an embedded JWT or a URL. - if url, err := url.Parse(i.Token); err == nil && url.Scheme != "" { + if u, err := url.Parse(i.Token); err == nil && u.Scheme != "" { c := &http.Client{Timeout: 5 * time.Second} - resp, err := c.Get(url.String()) + resp, err := c.Get(u.String()) if err != nil { - vr.AddWarning("import %s contains an unreachable token URL %q", i.Subject, i.Token) + vr.AddError("import %s contains an unreachable token URL %q", i.Subject, i.Token) } if resp != nil { defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - vr.AddWarning("import %s contains an unreadable token URL %q", i.Subject, i.Token) + vr.AddError("import %s contains an unreadable token URL %q", i.Subject, i.Token) } else { act, err = DecodeActivationClaims(string(body)) if err != nil { - vr.AddWarning("import %s contains a url %q with an invalid activation token", i.Subject, i.Token) + vr.AddError("import %s contains a URL %q with an invalid activation token", i.Subject, i.Token) } } } } else { var err error act, err = DecodeActivationClaims(i.Token) if err != nil { - vr.AddWarning("import %q contains an invalid activation token", i.Subject) + vr.AddError("import %q contains an invalid activation token", i.Subject) } } } if act != nil { - if act.Issuer != i.Account { - vr.AddWarning("activation token doesn't match account for import %q", i.Subject) + if !(act.Issuer == i.Account || act.IssuerAccount == i.Account) { + vr.AddError("activation token doesn't match account for import %q", i.Subject) } - if act.ClaimsData.Subject != actPubKey { - vr.AddWarning("activation token doesn't match account it is being included in, %q", i.Subject) + vr.AddError("activation token doesn't match account it is being included in, %q", i.Subject) + } + if act.ImportType != i.Type { + vr.AddError("mismatch between token import type %s and type of import %s", act.ImportType, i.Type) + } + act.validateWithTimeChecks(vr, false) + subj := i.Subject + if i.IsService() && i.To != "" { + subj = i.To + } + if !subj.IsContainedIn(act.ImportSubject) { + vr.AddError("activation token import subject %q doesn't match import %q", act.ImportSubject, i.Subject) } - } else { - vr.AddWarning("no activation provided for import %s", i.Subject) } - } // Imports is a list of import structs
imports_test.go+22 −47 modified@@ -34,29 +34,19 @@ func TestImportValidation(t *testing.T) { vr := CreateValidationResults() i.Validate("", vr) - if vr.IsEmpty() { - t.Errorf("imports without token or url should warn the caller") - } - - if vr.IsBlocking(true) { - t.Errorf("imports without token or url should not be blocking") + if !vr.IsEmpty() { + t.Errorf("imports should not generate an issue") } - i.Type = Service vr = CreateValidationResults() i.Validate("", vr) - if vr.IsEmpty() { - t.Errorf("imports without token or url should warn the caller") - } - - if vr.IsBlocking(true) { - t.Errorf("imports without token or url should not be blocking") + if !vr.IsEmpty() { + t.Errorf("imports should not generate an issue") } activation := NewActivationClaims(akp) - activation.Max = 1024 * 1024 - activation.Expires = time.Now().Add(time.Duration(time.Hour)).UTC().Unix() + activation.Expires = time.Now().Add(time.Hour).UTC().Unix() activation.ImportSubject = "test" activation.ImportType = Stream @@ -96,19 +86,15 @@ func TestInvalidImportToken(t *testing.T) { vr := CreateValidationResults() i.Validate("", vr) - if vr.IsEmpty() { - t.Errorf("imports with a bad token or url should warn the caller") - } - - if vr.IsBlocking(true) { - t.Errorf("invalid type shouldnt be blocking") + if !vr.IsBlocking(true) { + t.Errorf("bad token should be blocking") } } func TestInvalidImportURL(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) - i := &Import{Subject: "foo", Account: akp, Token: "foo://bad token url", To: "bar", Type: Stream} + i := &Import{Subject: "foo", Account: akp, Token: "foo://bad-token-url", To: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) @@ -117,8 +103,8 @@ func TestInvalidImportURL(t *testing.T) { t.Errorf("imports with a bad token or url should warn the caller") } - if vr.IsBlocking(true) { - t.Errorf("invalid type shouldnt be blocking") + if !vr.IsBlocking(true) { + t.Errorf("invalid type should be blocking") } } @@ -127,15 +113,11 @@ func TestInvalidImportTokenValuesValidation(t *testing.T) { ak2 := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) - i := &Import{Subject: "test", Account: akp2, To: "bar", Type: Stream} + i := &Import{Subject: "bar", Account: akp2, To: "test", Type: Service} vr := CreateValidationResults() i.Validate("", vr) - if vr.IsEmpty() { - t.Errorf("imports without token or url should warn the caller") - } - if vr.IsBlocking(true) { t.Errorf("imports without token or url should not be blocking") } @@ -144,10 +126,6 @@ func TestInvalidImportTokenValuesValidation(t *testing.T) { vr = CreateValidationResults() i.Validate("", vr) - if vr.IsEmpty() { - t.Errorf("imports without token or url should warn the caller") - } - if vr.IsBlocking(true) { t.Errorf("imports without token or url should not be blocking") } @@ -157,7 +135,7 @@ func TestInvalidImportTokenValuesValidation(t *testing.T) { activation.Expires = time.Now().Add(time.Duration(time.Hour)).UTC().Unix() activation.ImportSubject = "test" - activation.ImportType = Stream + activation.ImportType = Service actJWT := encode(activation, ak2, t) i.Token = actJWT @@ -187,18 +165,19 @@ func TestInvalidImportTokenValuesValidation(t *testing.T) { t.Errorf("imports with wrong issuer") } } + func TestMissingAccountInImport(t *testing.T) { i := &Import{Subject: "foo", To: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) - if len(vr.Issues) != 2 { - t.Errorf("imports without token or url should warn the caller, as should missing account") + if len(vr.Issues) != 1 { + t.Errorf("expected only one issue") } - if vr.IsBlocking(true) { - t.Errorf("Missing Account is not blocking, must import failures are warnings") + if !vr.IsBlocking(true) { + t.Errorf("Missing Account is blocking") } } @@ -210,10 +189,6 @@ func TestServiceImportWithWildcard(t *testing.T) { vr := CreateValidationResults() i.Validate("", vr) - if len(vr.Issues) != 2 { - t.Errorf("imports without token or url should warn the caller, as should wildcard service") - } - if !vr.IsBlocking(true) { t.Errorf("expected service import with a wildcard subject to be a blocking error") } @@ -225,8 +200,8 @@ func TestStreamImportWithWildcardPrefix(t *testing.T) { vr := CreateValidationResults() i.Validate("", vr) - if len(vr.Issues) != 3 { - t.Errorf("should have registered 3 issues with this import, got %d", len(vr.Issues)) + if len(vr.Issues) != 2 { + t.Errorf("should have registered 2 issues with this import, got %d", len(vr.Issues)) } if !vr.IsBlocking(true) { @@ -246,8 +221,8 @@ func TestImportsValidation(t *testing.T) { vr := CreateValidationResults() imports.Validate("", vr) - if len(vr.Issues) != 3 { - t.Errorf("imports without token or url should warn the caller x2, wildcard service as well") + if len(vr.Issues) != 1 { + t.Errorf("warn about wildcard service") } if !vr.IsBlocking(true) { @@ -349,7 +324,7 @@ func TestImportSubjectValidation(t *testing.T) { vr = CreateValidationResults() i.Validate(akp, vr) - if !vr.IsEmpty() { + if vr.IsEmpty() { t.Errorf("imports with non-contains subject should be not valid") }
v2/account_claims_test.go+1 −1 modified@@ -54,7 +54,7 @@ func TestNewAccountClaims(t *testing.T) { account.InfoURL = "http://localhost/my-account/doc" account.Description = "my account" account.Imports = Imports{} - account.Imports.Add(&Import{Subject: "test", Name: "test import", Account: apk2, Token: actJWT, To: "my", Type: Stream}) + account.Imports.Add(&Import{Subject: "test", Name: "test import", Account: apk2, Token: actJWT, LocalSubject: "my", Type: Stream}) vr := CreateValidationResults() account.Validate(vr)
v2/imports.go+16 −3 modified@@ -68,7 +68,11 @@ func (i *Import) Validate(actPubKey string, vr *ValidationResults) { } if i.Account == "" { - vr.AddWarning("account to import from is not specified") + vr.AddError("account to import from is not specified") + } + + if i.GetTo() != "" { + vr.AddWarning("the field to has been deprecated (use LocalSubject instead)") } i.Subject.Validate(vr) @@ -88,19 +92,28 @@ func (i *Import) Validate(actPubKey string, vr *ValidationResults) { var err error act, err = DecodeActivationClaims(i.Token) if err != nil { - vr.AddWarning("import %q contains an invalid activation token", i.Subject) + vr.AddError("import %q contains an invalid activation token", i.Subject) } } if act != nil { if !(act.Issuer == i.Account || act.IssuerAccount == i.Account) { vr.AddError("activation token doesn't match account for import %q", i.Subject) } - if act.ClaimsData.Subject != actPubKey { vr.AddError("activation token doesn't match account it is being included in, %q", i.Subject) } + if act.ImportType != i.Type { + vr.AddError("mismatch between token import type %s and type of import %s", act.ImportType, i.Type) + } act.validateWithTimeChecks(vr, false) + subj := i.Subject + if i.IsService() && i.To != "" { + subj = i.To + } + if !subj.IsContainedIn(act.ImportSubject) { + vr.AddError("activation token import subject %q doesn't match import %q", act.ImportSubject, i.Subject) + } } }
v2/imports_test.go+23 −27 modified@@ -27,7 +27,7 @@ func TestImportValidation(t *testing.T) { ak2 := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) - i := &Import{Subject: "test", Account: akp2, To: "bar", Type: Stream} + i := &Import{Subject: "test", Account: akp2, LocalSubject: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) @@ -36,7 +36,6 @@ func TestImportValidation(t *testing.T) { t.Errorf("imports should not generate an issue") } - i.Type = Service vr = CreateValidationResults() i.Validate("", vr) @@ -65,7 +64,7 @@ func TestImportValidationExpiredToken(t *testing.T) { ak2 := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) - i := &Import{Subject: "test", Account: akp2, To: "bar", Type: Stream} + i := &Import{Subject: "test", Account: akp2, LocalSubject: "bar", Type: Stream} // test success, expiration is not checked activation := NewActivationClaims(akp) activation.Expires = time.Now().Add(-time.Hour).UTC().Unix() @@ -117,7 +116,7 @@ func TestImportValidationSigningKey(t *testing.T) { ak2Sk := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) - i := &Import{Subject: "test", Account: akp2, To: "bar", Type: Stream} + i := &Import{Subject: "test", Account: akp2, LocalSubject: "bar", Type: Stream} activation := NewActivationClaims(akp) activation.Expires = time.Now().Add(time.Hour).UTC().Unix() @@ -158,18 +157,18 @@ func TestInvalidImportToken(t *testing.T) { i.Validate("", vr) if vr.IsEmpty() { - t.Errorf("imports with a bad token or url should warn the caller") + t.Errorf("imports with a bad token or url should cause an error") } - if vr.IsBlocking(true) { - t.Errorf("invalid type shouldnt be blocking") + if !vr.IsBlocking(false) { + t.Errorf("invalid type should be blocking") } } func TestInvalidImportURL(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) - i := &Import{Subject: "foo", Account: akp, Token: "foo://bad token url", To: "bar", Type: Stream} + i := &Import{Subject: "foo", Account: akp, Token: "foo://bad-token-url", To: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) @@ -178,8 +177,8 @@ func TestInvalidImportURL(t *testing.T) { t.Errorf("imports with a bad token or url should warn the caller") } - if vr.IsBlocking(true) { - t.Errorf("invalid type shouldnt be blocking") + if !vr.IsBlocking(true) { + t.Errorf("invalid type should be blocking") } } @@ -188,7 +187,7 @@ func TestInvalidImportTokenValuesValidation(t *testing.T) { ak2 := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) - i := &Import{Subject: "test", Account: akp2, To: "bar", Type: Stream} + i := &Import{Subject: "test", Account: akp2, LocalSubject: "bar", Type: Service} vr := CreateValidationResults() i.Validate("", vr) @@ -209,7 +208,7 @@ func TestInvalidImportTokenValuesValidation(t *testing.T) { activation.Expires = time.Now().Add(time.Hour).UTC().Unix() activation.ImportSubject = "test" - activation.ImportType = Stream + activation.ImportType = Service actJWT := encode(activation, ak2, t) i.Token = actJWT @@ -240,7 +239,7 @@ func TestInvalidImportTokenValuesValidation(t *testing.T) { } } func TestMissingAccountInImport(t *testing.T) { - i := &Import{Subject: "foo", To: "bar", Type: Stream} + i := &Import{Subject: "foo", LocalSubject: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) @@ -249,13 +248,13 @@ func TestMissingAccountInImport(t *testing.T) { t.Errorf("expected only one issue") } - if vr.IsBlocking(true) { - t.Errorf("Missing Account is not blocking, must import failures are warnings") + if !vr.IsBlocking(true) { + t.Errorf("Missing Account is blocking") } } func TestServiceImportWithWildcard(t *testing.T) { - i := &Import{Subject: "foo.*", Account: publicKey(createAccountNKey(t), t), To: "bar", Type: Service} + i := &Import{Subject: "foo.>", Account: publicKey(createAccountNKey(t), t), LocalSubject: "bar.>", Type: Service} vr := CreateValidationResults() i.Validate("", vr) @@ -274,7 +273,7 @@ func TestServiceImportWithWildcard(t *testing.T) { } func TestStreamImportWithWildcardPrefix(t *testing.T) { - i := &Import{Subject: "foo", Account: publicKey(createAccountNKey(t), t), To: "bar.*", Type: Stream} + i := &Import{Subject: "foo.>", Account: publicKey(createAccountNKey(t), t), LocalSubject: "bar.>", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) @@ -319,8 +318,8 @@ func TestStreamImportInformationSharing(t *testing.T) { func TestImportsValidation(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) - i := &Import{Subject: "foo", Account: akp, To: "bar", Type: Stream} - i2 := &Import{Subject: "foo.*", Account: akp, To: "bar", Type: Service} + i := &Import{Subject: "foo", Account: akp, LocalSubject: "bar", Type: Stream} + i2 := &Import{Subject: "foo.*", Account: akp, LocalSubject: "bar.*", Type: Service} imports := &Imports{} imports.Add(i, i2) @@ -391,10 +390,9 @@ func TestImportSubjectValidation(t *testing.T) { ak2 := createAccountNKey(t) akp2 := publicKey(ak2, t) - i := &Import{Subject: "one.two", Account: akp2, To: "bar", Type: Stream} + i := &Import{Subject: "one.two", Account: akp2, LocalSubject: "bar", Type: Stream} - actJWT := encode(activation, ak2, t) - i.Token = actJWT + i.Token = encode(activation, ak2, t) vr := CreateValidationResults() i.Validate(akp, vr) @@ -405,19 +403,17 @@ func TestImportSubjectValidation(t *testing.T) { activation.ImportSubject = "two" activation.ImportType = Stream - actJWT = encode(activation, ak2, t) - i.Token = actJWT + i.Token = encode(activation, ak2, t) vr = CreateValidationResults() i.Validate(akp, vr) - if !vr.IsEmpty() { + if vr.IsEmpty() { t.Errorf("imports with non-contains subject should be not valid") } activation.ImportSubject = ">" activation.ImportType = Stream - actJWT = encode(activation, ak2, t) - i.Token = actJWT + i.Token = encode(activation, ak2, t) vr = CreateValidationResults() i.Validate(akp, vr)
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-62mh-w5cv-p88cghsaADVISORY
- advisories.nats.io/CVE/CVE-2021-3127.txtghsax_refsource_MISCWEB
- github.com/nats-io/jwt/commit/6c72fdd73e82fa9ebb151d84773baf4e9164c4abghsaWEB
- github.com/nats-io/jwt/pull/149ghsaWEB
- github.com/nats-io/jwt/security/advisories/GHSA-62mh-w5cv-p88cghsaWEB
- github.com/nats-io/nats-server/security/advisories/GHSA-j756-f273-xhp4ghsaWEB
News mentions
0No linked articles in our index yet.