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:
-
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 thearray
parameter is true. -
validateClaimValues
function: This function is modified to include a check to ensure that all claim values are allowed. It usesvalues.every(v => allowed.some(a => a.test(v)))
to verify that each value in thevalues
array satisfies the condition of being allowed based on theallowed
array. -
Conditional Validation: Inside the
verifyToken
function, a conditional checkelse if (array)
is added. This check determines whether to usevalidateClaimArrayValues
orvalidateClaimValues
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:
-
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 theiss
claim for authentication or authorization. -
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.
-
Craft a Malicious JWT: The attacker crafts a JWT with a malicious
iss
claim. Theiss
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. -
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.
-
Submit the Malicious JWT: The attacker submits the crafted JWT to the target application.
-
Bypass Authentication/Authorization: If the application uses the
fast-jwt
library to verify the JWT and relies on theiss
claim for authentication or authorization, the vulnerable validation logic will incorrectly deem the JWT as valid because theiss
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:
-
Upgrade
fast-jwt
: The most effective mitigation is to upgrade tofast-jwt
version 5.0.6 or later. This version contains the fix for the vulnerability.npm install fast-jwt@latest
-
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 thefast-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 }
-
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. -
Regularly Review Dependencies: Regularly review your application's dependencies to identify and update any vulnerable libraries. Use tools like
npm audit
oryarn audit
to check for known vulnerabilities. -
Implement Robust Logging and Monitoring: Implement robust logging and monitoring to detect suspicious activity, such as the use of JWTs with unexpected
iss
claims. -
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.
-
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
- NVD: CVE-2025-30144
- GitHub Advisory: https://github.com/nearform/fast-jwt/security/advisories/GHSA-gm45-q3v2-6cf8
- fast-jwt Commit: https://github.com/nearform/fast-jwt/commit/cc26b1d473f900446ad846f8f0b10eb1c0adcbdd
- RFC 7519: https://datatracker.ietf.org/doc/html/rfc7519#page-9
- Vulert: https://vulert.com/vuln-db/npm-fast-jwt-186213
- OSV: https://osv.dev/vulnerability/GHSA-gm45-q3v2-6cf8