VYPR
Critical severityNVD Advisory· Published Mar 16, 2021· Updated Aug 3, 2024

CVE-2021-3127

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.

PackageAffected versionsPatched versions
github.com/nats-io/jwtGo
<= 1.2.2
github.com/nats-io/jwt/v2Go
< 2.0.12.0.1

Affected products

8

Patches

1
6c72fdd73e82

Merge pull request #149 from nats-io/cve

https://github.com/nats-io/jwtIvan KozlovicMar 14, 2021via ghsa
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

News mentions

0

No linked articles in our index yet.