CVE-2025-30144: fast-jwt Improper Issuer Claim Validation Vulnerability

Executive Summary

CVE-2025-30144 describes a critical vulnerability within the fast-jwt Node.js library, a popular package used for fast JSON Web Token (JWT) implementation. Versions prior to 5.0.6 are affected. The vulnerability stems from an improper validation of the iss (issuer) claim, as defined in RFC 7519. Specifically, the library incorrectly permits an array of strings as a valid iss value, deviating from the expected single string value. This flaw allows attackers to forge JWTs with malicious iss claims, potentially bypassing authentication and authorization mechanisms. A malicious actor can craft a JWT with an iss claim structured as ['https://attacker-domain/', 'https://valid-iss']. Due to the permissive validation, the JWT will be deemed valid. This can be further exploited if the application relies on external libraries like get-jwks that do not independently validate the iss claim. The vulnerability has a CVSS score of 6.5 (Medium).

Technical Details

  • Vulnerability Name: Improper Validation of iss Claim
  • CVE ID: CVE-2025-30144
  • Affected Software: fast-jwt Node.js library
  • Affected Versions: Versions prior to 5.0.6
  • Component: JWT validation logic, specifically the iss claim validation.
  • Attack Vector: Network
  • Complexity: High
  • Privileges Required: None
  • User Interaction: None
  • Scope: Unchanged
  • Confidentiality Impact: Low
  • Integrity Impact: High
  • Availability Impact: None
  • CWE: CWE-290 (Weak Authentication), CWE-345 (Insufficient Verification of Data Authenticity)

The fast-jwt library is designed for high-performance JWT processing in Node.js environments. JWTs are commonly used for authentication and authorization in web applications and APIs. The iss claim within a JWT identifies the issuer of the token. According to RFC 7519, the iss claim should be a single string value representing a URI. However, vulnerable versions of fast-jwt incorrectly allow the iss claim to be an array of strings.

This vulnerability affects systems that use fast-jwt to verify JWTs and rely on the iss claim for authentication or authorization decisions. If an application uses fast-jwt to validate JWTs received from a third-party identity provider, an attacker could potentially forge a JWT that appears to be issued by a trusted provider, even if it is not.

Root Cause Analysis

The root cause of CVE-2025-30144 lies in the insufficient validation logic for the iss claim within the fast-jwt library. The library's validation process failed to strictly enforce the RFC 7519 requirement that the iss claim must be a single string. Instead, it permitted an array of strings, opening the door for malicious actors to inject unauthorized issuers into the claim.

The vulnerable code resided within the verifier.js file, specifically in the functions responsible for claim validation. The original implementation lacked a check to ensure that the iss claim was not an array when it was expected to be a single string.

The following code snippet illustrates the flawed logic (this is a simplified representation based on the vulnerability description):

// Vulnerable code (simplified)
function verifyToken(token, options) {
  const decoded = decodeToken(token);
  const payload = decoded.payload;

  if (options.allowedIss) {
    const allowedIssuers = options.allowedIss;
    const issuer = payload.iss;

    // Vulnerable check: Allows array of strings
    if (!allowedIssuers.includes(issuer)) {
      throw new Error('Invalid issuer');
    }
  }

  return payload;
}

In this simplified example, the allowedIssuers.includes(issuer) check does not account for the possibility that issuer might be an array. If issuer is an array containing a valid issuer alongside a malicious one, the includes method might return true if the array contains the valid issuer, effectively bypassing the intended validation.

Patch Analysis

The fix for CVE-2025-30144, introduced in fast-jwt version 5.0.6, addresses the improper iss claim validation by adding stricter checks to ensure that claims like jti, iss, sub, and nonce are valid. The patch focuses on validating the type and values of these claims, particularly when they are expected to be single strings.

The following diff block shows the changes made to src/verifier.js:

--- a/src/verifier.js
+++ b/src/verifier.js
@@ -152,6 +152,16 @@ function validateClaimType(values, claim, array, type) {
 }
 
 function validateClaimValues(values, claim, allowed, arrayValue) {
+  const failureMessage = arrayValue
+    ? `Not all of the ${claim} claim values are allowed.`
+    : `The ${claim} claim value is not allowed.`
+
+  if (!values.every(v => allowed.some(a => a.test(v)))) {
+    throw new TokenError(TokenError.codes.invalidClaimValue, failureMessage)
+  }
+}
+
+function validateClaimArrayValues(values, claim, allowed, arrayValue) {
   const failureMessage = arrayValue
     ? `None of ${claim} claim values are allowed.`
     : `The ${claim} claim value is not allowed.`
@@ -222,6 +232,8 @@ function verifyToken(
 
     if (type === 'date') {
       validateClaimDateValue(value, modifier, now, greater, errorCode, errorVerb)
+    } else if (array) {
+      validateClaimArrayValues(values, claim, allowed, arrayValue)
     } else {
       validateClaimValues(values, claim, allowed, arrayValue)
     }

Explanation of Changes:

  1. validateClaimArrayValues function: This new function is introduced to handle the validation of claim values when the claim is expected to be an array. It checks if none of the claim values are allowed, throwing an error if that's the case. This function is used when the array parameter is true.

  2. validateClaimValues function: This function is modified to include a check to ensure that all claim values are allowed. It uses values.every(v => allowed.some(a => a.test(v))) to verify that each value in the values array satisfies the condition of being allowed based on the allowed array.

  3. Conditional Validation: Inside the verifyToken function, a conditional check else if (array) is added. This check determines whether to use validateClaimArrayValues or validateClaimValues based on whether the claim is expected to be an array or a single value.

The following diff block shows the changes made to test/verifier.spec.js:

--- a/test/verifier.spec.js
+++ b/test/verifier.spec.js
@@ -500,6 +500,41 @@ test('it validates the jti claim only if explicitily enabled', t => {
     { message: 'The jti claim value is not allowed.' }
   )
 
+  t.assert.throws(
+    () => {
+      return verify(
+        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOlsiSlRJIiwiSlRJMSJdLCJhdWQiOlsiQVVEMSJdLCJpc3MiOiJJU1MiLCJzdWIiOiJTVUIiLCJub25jZSI6Ik5PTkNFIn0.H2GACKIYvauUswRaK3SVsSwUOTjEcQDb1Qj_iCuLWoM',
+        { allowedJti: ['JTI'], key: 'secret-secret-secret-secret-secret' }
+      )
+    },
+    { message: 'Not all of the jti claim values are allowed.' }
+  )
+
+  t.assert.throws(
+    () => {
+      return verify(
+        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOlsiSlRJIiwiSlRJMSJdLCJhdWQiOlsiQVVEMSJdLCJpc3MiOiJJU1MiLCJzdWIiOiJTVUIiLCJub25jZSI6Ik5PTkNFIn0.H2GACKIYvauUswRaK3SVsSwUOTjEcQDb1Qj_iCuLWoM',
+        { allowedJti: ['JTI', 'JTI2'], key: 'secret-secret-secret-secret-secret' }
+      )
+    },
+    { message: 'Not all of the jti claim values are allowed.' }
+  )
+
+  t.assert.deepStrictEqual(
+    verify(
+      'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOlsiSlRJIiwiSlRJMSJdLCJhdWQiOlsiQVVEMSJdLCJpc3MiOiJJU1MiLCJzdWIiOiJTVUIiLCJub25jZSI6Ik5PTkNFIn0.H2GACKIYvauUswRaK3SVsSwUOTjEcQDb1Qj_iCuLWoM',
+      { allowedJti: ['JTI', 'JTI1'], key: 'secret-secret-secret-secret-secret' }
+    ),
+    {
+      a: 1,
+      jti: ['JTI', 'JTI1'],
+      aud: ['AUD1'],
+      iss: 'ISS',
+      sub: 'SUB',
+      nonce: 'NONCE'
+    }
+  )
+
   t.assert.deepStrictEqual(
     verify(
       'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOiJKVEkiLCJhdWQiOlsiQVVEMSIsIkRVQTIiXSwiaXNzIjoiSVNTIiwic3ViIjoiU1VCIiwibm9uY2UiOiJOT05DRSJ9.8fqzi23J-GjaD7rW3OYJv8UtBYkx8MOkViJjS4sXmVw',
@@ -664,6 +699,41 @@ test('it validates the iss claim only if explicitily enabled', t => {
     { message: 'The iss claim value is not allowed.' }
   )
 
+  t.assert.throws(
+    () => {
+      return verify(
+        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOiJKVEkiLCJhdWQiOlsiQVVEMSJdLCJpc3MiOlsiSVNTIiwiSVNTMSJdLCJzdWIiOiJTVUIiLCJub25jZSI6Ik5PTkNFIn0.IS9XILuqYEAKycN8j2MT0121j19T02CbW_h0erVh5IE',
+        { allowedIss: ['ISS'], key: 'secret-secret-secret-secret-secret' }
+      )
+    },
+    { message: 'Not all of the iss claim values are allowed.' }
+  )
+
+  t.assert.throws(
+    () => {
+      return verify(
+        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOiJKVEkiLCJhdWQiOlsiQVVEMSJdLCJpc3MiOlsiSVNTIiwiSVNTMSJdLCJzdWIiOiJTVUIiLCJub25jZSI6Ik5PTkNFIn0.IS9XILuqYEAKycN8j2MT0121j19T02CbW_h0erVh5IE',
+        { allowedIss: ['ISS', 'ISS2'], key: 'secret-secret-secret-secret-secret' }
+      )
+    },
+    { message: 'Not all of the iss claim values are allowed.' }
+  )
+
+  t.assert.deepStrictEqual(
+    verify(
+      'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOiJKVEkiLCJhdWQiOlsiQVVEMSJdLCJpc3MiOlsiSVNTIiwiSVNTMSJdLCJzdWIiOiJTVUIiLCJub25jZSI6Ik5PTkNFIn0.IS9XILuqYEAKycN8j2MT0121j19T02CbW_h0erVh5IE',
+      { allowedIss: ['ISS', 'ISS1'], key: 'secret-secret-secret-secret-secret' }
+    ),
+    {
+      a: 1,
+      jti: 'JTI',
+      aud: ['AUD1'],
+      iss: ['ISS', 'ISS1'],
+      sub: 'SUB',
+      nonce: 'NONCE'
+    }
+  )
+
   t.assert.deepStrictEqual(
     verify(
       'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOiJKVEkiLCJhdWQiOlsiQVVEMSIsIkRVQTIiXSwiaXNzIjoiSVNTIiwic3ViIjoiU1VCIiwibm9uY2UiOiJOT05DRSJ9.8fqzi23J-GjaD7rW3OYJv8UtBYkx8MOkViJjS4sXmVw',
@@ -741,6 +811,41 @@ test('it validates the sub claim only if explicitily enabled', t => {
     { message: 'The sub claim value is not allowed.' }
   )
 
+  t.assert.throws(
+    () => {
+      return verify(
+        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOiJKVEkiLCJhdWQiOlsiQVVEMSJdLCJpc3MiOiJJU1MiLCJzdWIiOlsiU1VCMSIsIlNVQjIiXSwibm9uY2UiOiJOT05DRSJ9.RwBpdTCEFCxO0jIFPnJpxjRd0JVIhP2Eettmsh0uwzY',
+        { allowedSub: ['SUB1'], key: 'secret-secret-secret-secret-secret' }
+      )
+    },
+    { message: 'Not all of the sub claim values are allowed.' }
+  )
+
+  t.assert.throws(
+    () => {
+      return verify(
+        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOiJKVEkiLCJhdWQiOlsiQVVEMSJdLCJpc3MiOiJJU1MiLCJzdWIiOlsiU1VCMSIsIlNVQjIiXSwibm9uY2UiOiJOT05DRSJ9.RwBpdTCEFCxO0jIFPnJpxjRd0JVIhP2Eettmsh0uwzY',
+        { allowedSub: ['SUB1', 'SUB3'], key: 'secret-secret-secret-secret-secret' }
+      )
+    },
+    { message: 'Not all of the sub claim values are allowed.' }
+  )
+
+  t.assert.deepStrictEqual(
+    verify(
+      'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOiJKVEkiLCJhdWQiOlsiQVVEMSJdLCJpc3MiOiJJU1MiLCJzdWIiOlsiU1VCMSIsIlNVQjIiXSwibm9uY2UiOiJOT05DRSJ9.RwBpdTCEFCxO0jIFPnJpxjRd0JVIhP2Eettmsh0uwzY',
+      { allowedSub: ['SUB1', 'SUB2'], key: 'secret-secret-secret-secret-secret' }
+    ),
+    {
+      a: 1,
+      jti: 'JTI',
+      aud: ['AUD1'],
+      iss: 'ISS',
+      sub: ['SUB1', 'SUB2'],
+      nonce: 'NONCE'
+    }
+  )
+
   t.assert.deepStrictEqual(
     verify(
       'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOiJKVEkiLCJhdWQiOlsiQVVEMSIsIkRVQTIiXSwiaXNzIjoiSVNTIiwic3ViIjoiU1VCIiwibm9uY2UiOiJOT05DRSJ9.8fqzi23J-GjaD7rW3OYJv8UtBYkx8MOkViJjS4sXmVw',
@@ -818,6 +923,41 @@ test('it validates the nonce claim only if explicitily enabled', t => {
     { message: 'The nonce claim value is not allowed.' }
   )
 
+  t.assert.throws(
+    () => {
+      return verify(
+        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOiJKVEkiLCJhdWQiOlsiQVVEMSJdLCJpc3MiOiJJU1MiLCJzdWIiOiJTVUIiLCJub25jZSI6WyJOT05DRSIsIk5PTkNFMSJdfQ.a8ZSzXebJvaw32jyWgbBo9aeLNTgs_sqxD2llV4f8KQ',
+        { allowedNonce: ['NONCE'], key: 'secret-secret-secret-secret-secret' }
+      )
+    },
+    { message: 'Not all of the nonce claim values are allowed.' }
+  )
+
+  t.assert.throws(
+    () => {
+      return verify(
+        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOiJKVEkiLCJhdWQiOlsiQVVEMSJdLCJpc3MiOiJJU1MiLCJzdWIiOiJTVUIiLCJub25jZSI6WyJOT05DRSIsIk5PTkNFMSJdfQ.a8ZSzXebJvaw32jyWgbBo9aeLNTgs_sqxD2llV4f8KQ',
+        { allowedNonce: ['NONCE', 'NONCE2'], key: 'secret-secret-secret-secret-secret' }
+      )
+    },
+    { message: 'Not all of the nonce claim values are allowed.' }
+  )
+
+  t.assert.deepStrictEqual(
+    verify(
+      'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOiJKVEkiLCJhdWQiOlsiQVVEMSJdLCJpc3MiOiJJU1MiLCJzdWIiOiJTVUIiLCJub25jZSI6WyJOT05DRSIsIk5PTkNFMSJdfQ.a8ZSzXebJvaw32jyWgbBo9aeLNTgs_sqxD2llV4f8KQ',
+      { allowedNonce: ['NONCE', 'NONCE1'], key: 'secret-secret-secret-secret-secret' }
+    ),
+    {
+      a: 1,
+      jti: 'JTI',
+      aud: ['AUD1'],
+      iss: 'ISS',
+      sub: 'SUB',
+      nonce: ['NONCE', 'NONCE1']
+    }
+  )
+
   t.assert.deepStrictEqual(
     verify(
       'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJqdGkiOiJKVEkiLCJhdWQiOlsiQVVEMSIsIkRVQTIiXSwiaXNzIjoiSVNTIiwic3ViIjoiU1VCIiwibm9uY2UiOiJOT05DRSJ9.8fqzi23J-GjaD7rW3OYJv8UtBYkx8MOkViJjS4sXmVw',

Explanation of Changes:

The test suite is expanded to include new test cases that specifically target the validation of jti, iss, sub, and nonce claims when they are provided as arrays. These tests assert that the verify function now correctly throws an error if not all values in the array are allowed, ensuring stricter validation of these claims. The tests also verify that when all values in the array are allowed, the verify function correctly parses the token and returns the expected payload.

In summary, the patch introduces stricter validation logic to ensure that claims like jti, iss, sub, and nonce adhere to the expected format and values, preventing attackers from exploiting the vulnerability by providing malicious or unexpected claim values. The added test cases further enhance the robustness of the validation process and ensure that the library behaves as expected in various scenarios.

Exploitation Techniques

The vulnerability allows an attacker to forge JWTs that bypass the intended issuer validation. Here's a step-by-step breakdown of how an attacker can exploit this:

  1. Identify a Vulnerable Application: The attacker first needs to identify an application that uses a vulnerable version of fast-jwt (prior to 5.0.6) and relies on the iss claim for authentication or authorization.

  2. Obtain a Valid JWT Structure: The attacker needs to understand the expected structure of a valid JWT for the target application. This includes the header, payload claims, and signature algorithm.

  3. Craft a Malicious JWT: The attacker crafts a JWT with a malicious iss claim. The iss claim is constructed as an array containing both a malicious issuer and a legitimate issuer. For example: iss: ['https://attacker-domain/', 'https://valid-iss']. The other claims in the JWT can be populated with values that are either valid or irrelevant to the exploit.

  4. Sign the JWT: The attacker signs the JWT using the appropriate algorithm. If the application uses a symmetric key, the attacker needs to obtain this key (e.g., through information disclosure or other vulnerabilities). If the application uses an asymmetric key (e.g., RSA), the attacker might be able to use a "None" algorithm attack (if supported by the library and application configuration) or attempt to exploit weaknesses in the key management.

  5. Submit the Malicious JWT: The attacker submits the crafted JWT to the target application.

  6. Bypass Authentication/Authorization: If the application uses the fast-jwt library to verify the JWT and relies on the iss claim for authentication or authorization, the vulnerable validation logic will incorrectly deem the JWT as valid because the iss claim contains the legitimate issuer. This allows the attacker to bypass the intended security checks and gain unauthorized access.

Example Attack Scenario:

Consider an application that uses JWTs for authentication. The application expects JWTs to be issued by https://auth.example.com. An attacker can craft a JWT with the following payload:

{
  "sub": "user123",
  "name": "Attacker",
  "iss": ["https://attacker-domain/", "https://auth.example.com"],
  "iat": 1678886400,
  "exp": 1678890000
}

If the application uses a vulnerable version of fast-jwt, it will incorrectly validate this JWT because the iss claim contains https://auth.example.com. The attacker can then use this JWT to access resources that are protected by authentication.

Potential Real-World Impacts:

  • Account Takeover: An attacker could forge JWTs to impersonate legitimate users, gaining unauthorized access to their accounts.
  • Data Breach: If the application stores sensitive data, an attacker could use forged JWTs to access and exfiltrate this data.
  • Privilege Escalation: An attacker could forge JWTs to gain elevated privileges within the application, allowing them to perform administrative tasks or access restricted resources.
  • System Compromise: In some cases, an attacker could use forged JWTs to compromise the entire system, potentially leading to a complete loss of control.

Mitigation Strategies

To mitigate the risk of CVE-2025-30144, the following strategies are recommended:

  1. Upgrade fast-jwt: The most effective mitigation is to upgrade to fast-jwt version 5.0.6 or later. This version contains the fix for the vulnerability.

    npm install fast-jwt@latest
    
  2. Implement Custom Validation: As a temporary workaround, you can implement custom validation logic to ensure that the iss claim is a single string value and not an array. This can be done by adding a check before calling the fast-jwt verification function.

    function verifyJwt(token, options) {
      const decoded = decodeToken(token);
      const payload = decoded.payload;
    
      if (payload.iss && Array.isArray(payload.iss)) {
        throw new Error('Invalid iss claim: must be a string');
      }
    
      return verifyToken(token, options); // Call the fast-jwt verify function
    }
    
  3. Use Strict Schema Validation: Implement strict schema validation for JWTs to enforce the expected data types and formats for all claims, including the iss claim. This can help prevent attackers from injecting unexpected data into the JWT.

  4. Regularly Review Dependencies: Regularly review your application's dependencies to identify and update any vulnerable libraries. Use tools like npm audit or yarn audit to check for known vulnerabilities.

  5. Implement Robust Logging and Monitoring: Implement robust logging and monitoring to detect suspicious activity, such as the use of JWTs with unexpected iss claims.

  6. Principle of Least Privilege: Apply the principle of least privilege to limit the access rights of users and applications. This can help minimize the impact of a successful attack.

  7. Web Application Firewall (WAF): Deploy a Web Application Firewall (WAF) to detect and block malicious requests, including those containing forged JWTs. Configure the WAF to inspect the iss claim and reject any JWTs with invalid values.

Timeline of Discovery and Disclosure

  • Vulnerability Discovered: Unknown
  • Vulnerability Reported: Unknown
  • Patch Released: March 19, 2025 (fast-jwt version 5.0.6)
  • CVE Assigned: CVE-2025-30144
  • Public Disclosure: March 19, 2025

References

Read more