CVE-2023-49290 (published 2023-12-05T00:15Z)

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.

CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L)

Weakness information:
cwe_id: CWE-400
cwe_name: Uncontrolled Resource Consumption
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.
extended_description: 
            Limited resources include memory, file system storage, database connection pool entries, and CPU. If an attacker can trigger the allocation of these limited resources, but the number or size of the resources is not controlled, then the attacker could cause a denial of service that consumes all available resources. This would prevent valid users from accessing the product, and it could potentially have an impact on the surrounding environment. For example, a memory exhaustion attack against an application could slow down the application as well as its host operating system.
            There are at least three distinct scenarios which can commonly lead to resource exhaustion:
               
                  Lack of throttling for the number of allocated resources
                  Losing all references to a resource before reaching the shutdown stage
                  Not closing/returning a resource after processing
               
            Resource exhaustion problems are often result due to an incorrect implementation of the following situations:
               
                  Error conditions and other exceptional circumstances.
                  Confusion over which part of the program is responsible for releasing the resource.
               
         
         

Fix information:
repo_url: https://github.com/lestrrat-go/jwx
commit_details: repo_url: https://github.com/lestrrat-go/jwx
author: lestrrat
author_date: 2023-12-03 15:35:12+09:00
msg: Merge pull request from GHSA-7f9x-gw85-8grf
repository: repo_url: https://github.com/lestrrat-go/jwx
repo_name: lestrrat-go/jwx
description: Implementation of various JWx (Javascript Object Signing and Encryption/JOSE) technologies
repo_language: Go
owner: lestrrat-go
file_changes: filename: Changes
change_type: ModificationType.MODIFY
diff: @@ -4,6 +4,15 @@ 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

programming_language: Markdown
filename: jwe.go
change_type: ModificationType.MODIFY
diff: @@ -10,6 +10,7 @@ import (
 	"crypto/rsa"
 	"fmt"
 	"io"
+	"sync"
 
 	"github.com/lestrrat-go/blackmagic"
 	"github.com/lestrrat-go/jwx/v2/internal/base64"
@@ -24,6 +25,20 @@ import (
 	"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
@@ -702,6 +717,12 @@ func (dctx *decryptCtx) decryptContent(ctx context.Context, alg jwa.KeyEncryptio
 		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)

programming_language: Go
filename: jwe_test.go
change_type: ModificationType.MODIFY
diff: @@ -911,3 +911,51 @@ func TestGH1001(t *testing.T) {
 	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
+		}
+	}
+}

programming_language: Go
filename: options.yaml
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,9 @@
 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`
@@ -129,3 +132,10 @@ options:
       
       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

programming_language: Markdown
filename: options_gen.go
change_type: ModificationType.MODIFY
diff: @@ -62,6 +62,18 @@ type encryptOption struct {
 
 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
@@ -117,6 +129,7 @@ type identFS struct{}
 type identKey struct{}
 type identKeyProvider struct{}
 type identKeyUsed struct{}
+type identMaxPBES2Count struct{}
 type identMergeProtectedHeaders struct{}
 type identMessage struct{}
 type identPerRecipientHeaders struct{}
@@ -153,6 +166,10 @@ func (identKeyUsed) String() string {
 	return "WithKeyUsed"
 }
 
+func (identMaxPBES2Count) String() string {
+	return "WithMaxPBES2Count"
+}
+
 func (identMergeProtectedHeaders) String() string {
 	return "WithMergeProtectedHeaders"
 }
@@ -228,6 +245,13 @@ 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

programming_language: Go
filename: options_gen_test.go
change_type: ModificationType.MODIFY
diff: @@ -16,6 +16,7 @@ func TestOptionIdent(t *testing.T) {
 	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())

programming_language: Go