CVE-2024-21664: NULL Pointer Dereference (CWE-476)
value: jwx is a Go module implementing various JWx (JWA/JWE/JWK/JWS/JWT, otherwise known as JOSE) technologies. Calling `jws.Parse` with a JSON serialized payload where the `signature` field is present while `protected` is absent can lead to a nil pointer dereference. The vulnerability can be used to crash/DOS a system doing JWS verification. This vulnerability has been patched in versions 2.0.19 and 1.2.28.

CWE description: The product dereferences a pointer that it expects to be valid but is NULL.
CVSS v3 base score: 7.5
Published: 2024-01-09T20:15Z
Repository: https://github.com/lestrrat-go/jwx
Language: Go
Files changed: Changes, jws/jws_test.go, jws/message.go
Commit date: 2024-01-09 09:45:34 +0900
Commit message: Merge pull request from GHSA-pvcr-v8j8-j5q3

* Add tests for empty protected headers

* check for sig.protected == nil

* Add one more case for missing protected headers in compact form

* Update Changes

* JWS: Check for sig.protected == nil on non-flattened input

---------

Co-authored-by: Fredrik Strupe <fredrik@strupe.net>
Diff: diff --git a/Changes b/Changes
index cd3dcca2..94568ebc 100644
--- a/Changes
+++ b/Changes
@@ -8,10 +8,20 @@ v2.0.19 UNRELEASED
 [New Features]
   * [jws] Added jws.IsVerificationError to check if the error returned by `jws.Verify`
     was caused by actual verification step or something else, for example, while fetching
     a key from datasource
 
+[Security Fixes]
+  * [jws] JWS messages formated in full JSON format (i.e. not the compact format, which
+    consists of three base64 strings concatenated with a '.') with missing "protected"
+    headers could cause a panic, thereby introducing a possiblity of a DoS.
+
+    This has been fixed so that the `jws.Parse` function succeeds in parsing a JWS message
+    lacking a protected header. Calling `jws.Verify` on this same JWS message will result
+    in a failed verification attempt. Note that this behavior will differ slightly when
+    parsing JWS messages in compact form, which result in an error. 
+
 v2.0.18 03 Dec 2023
 [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
diff --git a/jws/jws_test.go b/jws/jws_test.go
index d615ec37..9f92d305 100644
--- a/jws/jws_test.go
+++ b/jws/jws_test.go
@@ -1833,5 +1833,57 @@ func TestValidateKey(t *testing.T) {
 	// The following should now succeed, because N has been reinstated
 	require.NoError(t, pubKey.Set(jwk.RSANKey, n), `jwk.Set should succeed`)
 	_, err = jws.Verify(signed, jws.WithKey(jwa.RS256, pubKey), jws.WithValidateKey(true))
 	require.NoError(t, err, `jws.Verify should succeed`)
 }
+
+func TestEmptyProtectedField(t *testing.T) {
+	// MEMO: this was the only test case from the original report
+	// This passes. It should produce an invalid JWS message, but
+	// that's not `jws.Parse`'s problem.
+	_, err := jws.Parse([]byte(`{"signature": ""}`))
+	require.NoError(t, err, `jws.Parse should fail`)
+
+	// Also test that non-flattened serialization passes.
+	_, err = jws.Parse([]byte(`{"signatures": [{}]}`))
+	require.NoError(t, err, `jws.Parse should fail`)
+
+	// MEMO: rest of the cases are present to be extra pedantic about it
+
+	privKey, err := jwxtest.GenerateRsaJwk()
+	require.NoError(t, err, `jwxtest.GenerateRsaJwk should succeed`)
+
+	// This fails. `jws.Parse` works, but the subsequent verification
+	// workflow fails to verify anything without the presense of a signature or
+	// a protected header.
+	_, err = jws.Verify([]byte(`{"signature": ""}`), jws.WithKey(jwa.RS256, privKey))
+	require.Error(t, err, `jws.Parse should fail`)
+
+	// Create a valid signatre.
+	signed, err := jws.Sign([]byte("Lorem Ipsum"), jws.WithKey(jwa.RS256, privKey))
+	require.NoError(t, err, `jws.Sign should succeed`)
+
+	_, payload, signature, err := jws.SplitCompact(signed)
+	require.NoError(t, err, `jws.SplitCompact should succeed`)
+
+	// This fails as well. we have a valid signature and a valid
+	// key to verify it, but no protected headers
+	_, err = jws.Verify(
+		[]byte(fmt.Sprintf(`{"signature": "%s"}`, signature)),
+		jws.WithKey(jwa.RS256, privKey),
+	)
+	require.Error(t, err, `jws.Verify should fail`)
+
+	// Test for cases when we have an incomplete compact form JWS
+	var buf bytes.Buffer
+	buf.WriteRune('.')
+	buf.Write(payload)
+	buf.WriteRune('.')
+	buf.Write(signature)
+	invalidMessage := buf.Bytes()
+
+	// This is an error because the format is simply wrong.
+	// Whereas in the other JSON-based JWS's case the lack of protected field
+	// is not a SYNTAX error, this one is, and therefore we barf.
+	_, err = jws.Parse(invalidMessage)
+	require.Error(t, err, `jws.Parse should fail`)
+}
diff --git a/jws/message.go b/jws/message.go
index adec8445..dca5dfeb 100644
--- a/jws/message.go
+++ b/jws/message.go
@@ -276,10 +276,15 @@ func (m *Message) UnmarshalJSON(buf []byte) error {
 			if err := json.Unmarshal(rawsig, &sig); err != nil {
 				return fmt.Errorf(`failed to unmarshal signature #%d: %w`, i+1, err)
 			}
 			sig.SetDecodeCtx(nil)
 
+			if sig.protected == nil {
+				// Instead of barfing on a nil protected header, use an empty header
+				sig.protected = NewHeaders()
+			}
+
 			if i == 0 {
 				if !getB64Value(sig.protected) {
 					b64 = false
 				}
 			} else {
@@ -311,10 +316,15 @@ func (m *Message) UnmarshalJSON(buf []byte) error {
 			//nolint:forcetypeassert
 			prt.(*stdHeaders).SetDecodeCtx(nil)
 			sig.protected = prt
 		}
 
+		if sig.protected == nil {
+			// Instead of barfing on a nil protected header, use an empty header
+			sig.protected = NewHeaders()
+		}
+
 		decoded, err := base64.DecodeString(*mup.Signature)
 		if err != nil {
 			return fmt.Errorf(`failed to base64 decode flattened signature: %w`, err)
 		}
 		sig.signature = decoded

Fixed code: [Security Fixes]
  * [jws] JWS messages formated in full JSON format (i.e. not the compact format, which
    consists of three base64 strings concatenated with a '.') with missing "protected"
    headers could cause a panic, thereby introducing a possiblity of a DoS.

    This has been fixed so that the `jws.Parse` function succeeds in parsing a JWS message
    lacking a protected header. Calling `jws.Verify` on this same JWS message will result
    in a failed verification attempt. Note that this behavior will differ slightly when
    parsing JWS messages in compact form, which result in an error. 


func TestEmptyProtectedField(t *testing.T) {
	// MEMO: this was the only test case from the original report
	// This passes. It should produce an invalid JWS message, but
	// that's not `jws.Parse`'s problem.
	_, err := jws.Parse([]byte(`{"signature": ""}`))
	require.NoError(t, err, `jws.Parse should fail`)

	// Also test that non-flattened serialization passes.
	_, err = jws.Parse([]byte(`{"signatures": [{}]}`))
	require.NoError(t, err, `jws.Parse should fail`)

	// MEMO: rest of the cases are present to be extra pedantic about it

	privKey, err := jwxtest.GenerateRsaJwk()
	require.NoError(t, err, `jwxtest.GenerateRsaJwk should succeed`)

	// This fails. `jws.Parse` works, but the subsequent verification
	// workflow fails to verify anything without the presense of a signature or
	// a protected header.
	_, err = jws.Verify([]byte(`{"signature": ""}`), jws.WithKey(jwa.RS256, privKey))
	require.Error(t, err, `jws.Parse should fail`)

	// Create a valid signatre.
	signed, err := jws.Sign([]byte("Lorem Ipsum"), jws.WithKey(jwa.RS256, privKey))
	require.NoError(t, err, `jws.Sign should succeed`)

	_, payload, signature, err := jws.SplitCompact(signed)
	require.NoError(t, err, `jws.SplitCompact should succeed`)

	// This fails as well. we have a valid signature and a valid
	// key to verify it, but no protected headers
	_, err = jws.Verify(
		[]byte(fmt.Sprintf(`{"signature": "%s"}`, signature)),
		jws.WithKey(jwa.RS256, privKey),
	)
	require.Error(t, err, `jws.Verify should fail`)

	// Test for cases when we have an incomplete compact form JWS
	var buf bytes.Buffer
	buf.WriteRune('.')
	buf.Write(payload)
	buf.WriteRune('.')
	buf.Write(signature)
	invalidMessage := buf.Bytes()

	// This is an error because the format is simply wrong.
	// Whereas in the other JSON-based JWS's case the lack of protected field
	// is not a SYNTAX error, this one is, and therefore we barf.
	_, err = jws.Parse(invalidMessage)
	require.Error(t, err, `jws.Parse should fail`)
}
			if sig.protected == nil {
				// Instead of barfing on a nil protected header, use an empty header
				sig.protected = NewHeaders()
			}

		if sig.protected == nil {
			// Instead of barfing on a nil protected header, use an empty header
			sig.protected = NewHeaders()
		}