CVE-2023-49290: Uncontrolled Resource Consumption (CWE-400)
value: lestrrat-go/jwx is a Go module implementing various JWx (JWA/JWE/JWK/JWS/JWT, otherwise known as JOSE) technologies. A p2c parameter set too high in JWE's algorithm PBES2-* could lead to a denial of service. The JWE key management algorithms based on PBKDF2 require a JOSE Header Parameter called p2c (PBES2 Count). This parameter dictates the number of PBKDF2 iterations needed to derive a CEK wrapping key. Its primary purpose is to intentionally slow down the key derivation function, making password brute-force and dictionary attacks more resource- intensive. Therefore, if an attacker sets the p2c parameter in JWE to a very large number, it can cause a lot of computational consumption, resulting in a denial of service. This vulnerability has been addressed in commit `64f2a229b` which has been included in release version 1.2.27 and 2.0.18. Users are advised to upgrade. There are no known workarounds for this vulnerability.
CWE description: The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
CVSS v3 base score: 5.3
Published: 2023-12-05T00:15Z
Repository: https://github.com/lestrrat-go/jwx
Language: Go
Files changed: Changes, jwe/jwe.go, jwe/jwe_test.go, jwe/options.yaml, jwe/options_gen.go, jwe/options_gen_test.go
Commit date: 2023-12-03 15:35:12 +0900
Commit message: Merge pull request from GHSA-7f9x-gw85-8grf


Diff: diff --git a/Changes b/Changes
index 9026242..36d2963 100644
--- a/Changes
+++ b/Changes
@@ -2,10 +2,19 @@ Changes
 =======
 
 v2 has many incompatibilities with v1. To see the full list of differences between
 v1 and v2, please read the Changes-v2.md file (https://github.com/lestrrat-go/jwx/blob/develop/v2/Changes-v2.md)
 
+v2.0.18 UNRELEASED
+[Security Fixes]
+  * [jwe] A large number in p2c parameter for PBKDF2 based encryptions could cause a DoS attack,
+    similar to https://nvd.nist.gov/vuln/detail/CVE-2022-36083.  All users who use JWE via this
+    package should upgrade. While the JOSE spec allows for encryption using JWE on JWTs, users of
+    the `jwt` package are not immediately susceptible unless they explicitly try to decrypt
+    JWTs -- by default the `jwt` package verifies signatures, but does not decrypt messages.
+    [GHSA-7f9x-gw85-8grf]
+
 v2.0.17 20 Nov 2023
 [Bug Fixes]
   * [jws] Previously, `jws.UnregisterSigner` did not remove the previous signer instance when
     the signer was registered and unregistered multiple times (#1016). This has been fixed.
 
diff --git a/jwe/jwe.go b/jwe/jwe.go
index 4a7cb19..3b64db6 100644
--- a/jwe/jwe.go
+++ b/jwe/jwe.go
@@ -8,10 +8,11 @@ import (
 	"context"
 	"crypto/ecdsa"
 	"crypto/rsa"
 	"fmt"
 	"io"
+	"sync"
 
 	"github.com/lestrrat-go/blackmagic"
 	"github.com/lestrrat-go/jwx/v2/internal/base64"
 	"github.com/lestrrat-go/jwx/v2/internal/json"
 	"github.com/lestrrat-go/jwx/v2/internal/keyconv"
@@ -22,10 +23,24 @@ import (
 	"github.com/lestrrat-go/jwx/v2/jwe/internal/keyenc"
 	"github.com/lestrrat-go/jwx/v2/jwe/internal/keygen"
 	"github.com/lestrrat-go/jwx/v2/x25519"
 )
 
+var muSettings sync.RWMutex
+var maxPBES2Count = 10000
+
+func Settings(options ...GlobalOption) {
+	muSettings.Lock()
+	defer muSettings.Unlock()
+	for _, option := range options {
+		switch option.Ident() {
+		case identMaxPBES2Count{}:
+			maxPBES2Count = option.Value().(int)
+		}
+	}
+}
+
 const (
 	fmtInvalid = iota
 	fmtCompact
 	fmtJSON
 	fmtJSONPretty
@@ -700,10 +715,16 @@ func (dctx *decryptCtx) decryptContent(ctx context.Context, alg jwa.KeyEncryptio
 		}
 		countFlt, ok := count.(float64)
 		if !ok {
 			return nil, fmt.Errorf("unexpected type for 'p2c': %T", count)
 		}
+		muSettings.RLock()
+		maxCount := maxPBES2Count
+		muSettings.RUnlock()
+		if countFlt > float64(maxCount) {
+			return nil, fmt.Errorf("invalid 'p2c' value")
+		}
 		salt, err := base64.DecodeString(saltB64Str)
 		if err != nil {
 			return nil, fmt.Errorf(`failed to b64-decode 'salt': %w`, err)
 		}
 		dec.KeySalt(salt)
diff --git a/jwe/jwe_test.go b/jwe/jwe_test.go
index 59796e6..28cbb3d 100644
--- a/jwe/jwe_test.go
+++ b/jwe/jwe_test.go
@@ -909,5 +909,53 @@ func TestGH1001(t *testing.T) {
 	require.NoError(t, err, `jwe.Decrypt should succeed`)
 
 	require.Equal(t, "Lorem Ipsum", string(decrypted), `decrypted message should match`)
 	require.NotNil(t, cek, `cek should not be nil`)
 }
+
+func TestGHSA_7f9x_gw85_8grf(t *testing.T) {
+	token := []byte("eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJjIjoyMDAwMDAwMDAwLCJwMnMiOiJNNzczSnlmV2xlX2FsSXNrc0NOTU9BIn0=.S8B1kXdIR7BM6i_TaGsgqEOxU-1Sgdakp4mHq7UVhn-_REzOiGz2gg.gU_LfzhBXtQdwYjh.9QUIS-RWkLc.m9TudmzUoCzDhHsGGfzmCA")
+	key, err := jwk.FromRaw([]byte(`abcdefg`))
+	require.NoError(t, err, `jwk.FromRaw should succeed`)
+
+	{
+		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+		defer cancel()
+
+		done := make(chan struct{})
+		go func(t *testing.T, done chan struct{}) {
+			_, err := jwe.Decrypt(token, jwe.WithKey(jwa.PBES2_HS256_A128KW, key))
+			require.Error(t, err, `jwe.Decrypt should fail`)
+			close(done)
+		}(t, done)
+
+		select {
+		case <-done:
+		case <-ctx.Done():
+			require.Fail(t, "jwe.Decrypt should not block")
+		}
+	}
+
+	// NOTE: HAS GLOBAL EFFECT
+	// Should allow for timeout to occur
+	jwe.Settings(jwe.WithMaxPBES2Count(100000000000000000))
+
+	// put it back to normal after the test
+	defer jwe.Settings(jwe.WithMaxPBES2Count(10000))
+	{
+		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+		defer cancel()
+
+		done := make(chan struct{})
+		go func(t *testing.T, done chan struct{}) {
+			_, _ = jwe.Decrypt(token, jwe.WithKey(jwa.PBES2_HS256_A128KW, key))
+			close(done)
+		}(t, done)
+
+		select {
+		case <-done:
+			require.Fail(t, "jwe.Decrypt should block")
+		case <-ctx.Done():
+			// timeout occurred as it should
+		}
+	}
+}
diff --git a/jwe/options.yaml b/jwe/options.yaml
index 623a186..bf7e0a0 100644
--- a/jwe/options.yaml
+++ b/jwe/options.yaml
@@ -1,8 +1,11 @@
 package_name: jwe
 output: jwe/options_gen.go
 interfaces:
+  - name: GlobalOption
+    comment: |
+      GlobalOption describes options that changes global settings for this package
   - name: CompactOption
     comment: |
       CompactOption describes options that can be passed to `jwe.Compact`
   - name: DecryptOption
     comment: |
@@ -127,5 +130,12 @@ options:
       message upon successful decryption. The variable must be a pointer to
       a byte slice, and it will only be populated if the decryption is successful.
       
       This option is currently considered EXPERIMENTAL, and is subject to
       future changes across minor/micro versions.
+  - ident: MaxPBES2Count
+    interface: GlobalOption
+    argument_type: int
+    comment: |
+      WithMaxPBES2Count specifies the maximum number of PBES2 iterations
+      to use when decrypting a message. If not specified, the default
+      value of 10,000 is used.
\ No newline at end of file
diff --git a/jwe/options_gen.go b/jwe/options_gen.go
index cdb22be..b3de132 100644
--- a/jwe/options_gen.go
+++ b/jwe/options_gen.go
@@ -60,10 +60,22 @@ type encryptOption struct {
 	Option
 }
 
 func (*encryptOption) encryptOption() {}
 
+// GlobalOption describes options that changes global settings for this package
+type GlobalOption interface {
+	Option
+	globalOption()
+}
+
+type globalOption struct {
+	Option
+}
+
+func (*globalOption) globalOption() {}
+
 // ReadFileOption is a type of `Option` that can be passed to `jwe.Parse`
 type ParseOption interface {
 	Option
 	readFileOption()
 }
@@ -115,10 +127,11 @@ type identCompress struct{}
 type identContentEncryptionAlgorithm struct{}
 type identFS struct{}
 type identKey struct{}
 type identKeyProvider struct{}
 type identKeyUsed struct{}
+type identMaxPBES2Count struct{}
 type identMergeProtectedHeaders struct{}
 type identMessage struct{}
 type identPerRecipientHeaders struct{}
 type identPretty struct{}
 type identProtectedHeaders struct{}
@@ -151,10 +164,14 @@ func (identKeyProvider) String() string {
 
 func (identKeyUsed) String() string {
 	return "WithKeyUsed"
 }
 
+func (identMaxPBES2Count) String() string {
+	return "WithMaxPBES2Count"
+}
+
 func (identMergeProtectedHeaders) String() string {
 	return "WithMergeProtectedHeaders"
 }
 
 func (identMessage) String() string {
@@ -226,10 +243,17 @@ func WithKeyProvider(v KeyProvider) DecryptOption {
 // jwx API allows users to specify a raw key such as *rsa.PublicKey)
 func WithKeyUsed(v interface{}) DecryptOption {
 	return &decryptOption{option.New(identKeyUsed{}, v)}
 }
 
+// WithMaxPBES2Count specifies the maximum number of PBES2 iterations
+// to use when decrypting a message. If not specified, the default
+// value of 10,000 is used.
+func WithMaxPBES2Count(v int) GlobalOption {
+	return &globalOption{option.New(identMaxPBES2Count{}, v)}
+}
+
 // WithMergeProtectedHeaders specify that when given multiple headers
 // as options to `jwe.Encrypt`, these headers should be merged instead
 // of overwritten
 func WithMergeProtectedHeaders(v bool) EncryptOption {
 	return &encryptOption{option.New(identMergeProtectedHeaders{}, v)}
diff --git a/jwe/options_gen_test.go b/jwe/options_gen_test.go
index 16984e2..d36b976 100644
--- a/jwe/options_gen_test.go
+++ b/jwe/options_gen_test.go
@@ -14,10 +14,11 @@ func TestOptionIdent(t *testing.T) {
 	require.Equal(t, "WithContentEncryption", identContentEncryptionAlgorithm{}.String())
 	require.Equal(t, "WithFS", identFS{}.String())
 	require.Equal(t, "WithKey", identKey{}.String())
 	require.Equal(t, "WithKeyProvider", identKeyProvider{}.String())
 	require.Equal(t, "WithKeyUsed", identKeyUsed{}.String())
+	require.Equal(t, "WithMaxPBES2Count", identMaxPBES2Count{}.String())
 	require.Equal(t, "WithMergeProtectedHeaders", identMergeProtectedHeaders{}.String())
 	require.Equal(t, "WithMessage", identMessage{}.String())
 	require.Equal(t, "WithPerRecipientHeaders", identPerRecipientHeaders{}.String())
 	require.Equal(t, "WithPretty", identPretty{}.String())
 	require.Equal(t, "WithProtectedHeaders", identProtectedHeaders{}.String())

Fixed code: v2.0.18 UNRELEASED
[Security Fixes]
  * [jwe] A large number in p2c parameter for PBKDF2 based encryptions could cause a DoS attack,
    similar to https://nvd.nist.gov/vuln/detail/CVE-2022-36083.  All users who use JWE via this
    package should upgrade. While the JOSE spec allows for encryption using JWE on JWTs, users of
    the `jwt` package are not immediately susceptible unless they explicitly try to decrypt
    JWTs -- by default the `jwt` package verifies signatures, but does not decrypt messages.
    [GHSA-7f9x-gw85-8grf]

	"sync"
var muSettings sync.RWMutex
var maxPBES2Count = 10000

func Settings(options ...GlobalOption) {
	muSettings.Lock()
	defer muSettings.Unlock()
	for _, option := range options {
		switch option.Ident() {
		case identMaxPBES2Count{}:
			maxPBES2Count = option.Value().(int)
		}
	}
}

		muSettings.RLock()
		maxCount := maxPBES2Count
		muSettings.RUnlock()
		if countFlt > float64(maxCount) {
			return nil, fmt.Errorf("invalid 'p2c' value")
		}

func TestGHSA_7f9x_gw85_8grf(t *testing.T) {
	token := []byte("eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJjIjoyMDAwMDAwMDAwLCJwMnMiOiJNNzczSnlmV2xlX2FsSXNrc0NOTU9BIn0=.S8B1kXdIR7BM6i_TaGsgqEOxU-1Sgdakp4mHq7UVhn-_REzOiGz2gg.gU_LfzhBXtQdwYjh.9QUIS-RWkLc.m9TudmzUoCzDhHsGGfzmCA")
	key, err := jwk.FromRaw([]byte(`abcdefg`))
	require.NoError(t, err, `jwk.FromRaw should succeed`)

	{
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		done := make(chan struct{})
		go func(t *testing.T, done chan struct{}) {
			_, err := jwe.Decrypt(token, jwe.WithKey(jwa.PBES2_HS256_A128KW, key))
			require.Error(t, err, `jwe.Decrypt should fail`)
			close(done)
		}(t, done)

		select {
		case <-done:
		case <-ctx.Done():
			require.Fail(t, "jwe.Decrypt should not block")
		}
	}

	// NOTE: HAS GLOBAL EFFECT
	// Should allow for timeout to occur
	jwe.Settings(jwe.WithMaxPBES2Count(100000000000000000))

	// put it back to normal after the test
	defer jwe.Settings(jwe.WithMaxPBES2Count(10000))
	{
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		done := make(chan struct{})
		go func(t *testing.T, done chan struct{}) {
			_, _ = jwe.Decrypt(token, jwe.WithKey(jwa.PBES2_HS256_A128KW, key))
			close(done)
		}(t, done)

		select {
		case <-done:
			require.Fail(t, "jwe.Decrypt should block")
		case <-ctx.Done():
			// timeout occurred as it should
		}
	}
}
  - name: GlobalOption
    comment: |
      GlobalOption describes options that changes global settings for this package
  - ident: MaxPBES2Count
    interface: GlobalOption
    argument_type: int
    comment: |
      WithMaxPBES2Count specifies the maximum number of PBES2 iterations
      to use when decrypting a message. If not specified, the default
      value of 10,000 is used.
// GlobalOption describes options that changes global settings for this package
type GlobalOption interface {
	Option
	globalOption()
}

type globalOption struct {
	Option
}

func (*globalOption) globalOption() {}

type identMaxPBES2Count struct{}
func (identMaxPBES2Count) String() string {
	return "WithMaxPBES2Count"
}

// WithMaxPBES2Count specifies the maximum number of PBES2 iterations
// to use when decrypting a message. If not specified, the default
// value of 10,000 is used.
func WithMaxPBES2Count(v int) GlobalOption {
	return &globalOption{option.New(identMaxPBES2Count{}, v)}
}

	require.Equal(t, "WithMaxPBES2Count", identMaxPBES2Count{}.String())