Skip to content

Commit abbc307

Browse files
committed
Fix type confusion
1 parent e99740d commit abbc307

5 files changed

Lines changed: 71 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# Changelog
22

3-
## 10.3.0 (unreleased)
3+
## 10.3.0 (2026-01-27)
44

55
- Export everything needed to define your own CryptoProvider
6+
- Fix type confusion with exp/nbf when not required
67

78
## 10.2.0 (2025-11-06)
89

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "jsonwebtoken"
3-
version = "10.2.0"
3+
version = "10.3.0"
44
authors = ["Vincent Prouillet <hello@vincentprouillet.com>"]
55
license = "MIT"
66
readme = "README.md"

src/errors.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ pub enum ErrorKind {
5858
// Validation errors
5959
/// When a claim required by the validation is not present
6060
MissingRequiredClaim(String),
61+
/// When a claim has an invalid format (eg string instead of integer)
62+
InvalidClaimFormat(String),
6163
/// When a token’s `exp` claim indicates that it has expired
6264
ExpiredSignature,
6365
/// When a token’s `iss` claim does not match the expected issuer
@@ -98,6 +100,7 @@ impl StdError for Error {
98100
ErrorKind::ExpiredSignature => None,
99101
ErrorKind::MissingAlgorithm => None,
100102
ErrorKind::MissingRequiredClaim(_) => None,
103+
ErrorKind::InvalidClaimFormat(_) => None,
101104
ErrorKind::InvalidIssuer => None,
102105
ErrorKind::InvalidAudience => None,
103106
ErrorKind::InvalidSubject => None,
@@ -131,6 +134,7 @@ impl fmt::Display for Error {
131134
| ErrorKind::InvalidEddsaKey
132135
| ErrorKind::InvalidAlgorithmName => write!(f, "{:?}", self.0),
133136
ErrorKind::MissingRequiredClaim(c) => write!(f, "Missing required claim: {}", c),
137+
ErrorKind::InvalidClaimFormat(c) => write!(f, "Invalid format for claim: {}", c),
134138
ErrorKind::InvalidRsaKey(msg) => write!(f, "RSA key invalid: {}", msg),
135139
ErrorKind::Signing(msg) => write!(f, "Signing failed: {}", msg),
136140
ErrorKind::Json(err) => write!(f, "JSON error: {}", err),

src/validation.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,14 @@ pub(crate) fn validate(claims: ClaimsForValidation, options: &Validation) -> Res
274274
if options.validate_exp || options.validate_nbf {
275275
let now = get_current_timestamp();
276276

277+
// Reject malformed exp/nbf claim when validation is enabled
278+
if options.validate_exp && matches!(claims.exp, TryParse::FailedToParse) {
279+
return Err(new_error(ErrorKind::InvalidClaimFormat("exp".to_string())));
280+
}
281+
if options.validate_nbf && matches!(claims.nbf, TryParse::FailedToParse) {
282+
return Err(new_error(ErrorKind::InvalidClaimFormat("nbf".to_string())));
283+
}
284+
277285
if matches!(claims.exp, TryParse::Parsed(exp) if exp < options.reject_tokens_expiring_in_less_than)
278286
{
279287
return Err(new_error(ErrorKind::InvalidToken));

tests/hmac.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,59 @@ fn verify_hs256_rfc7517_appendix_a1() {
290290
let c = decode::<C>(token, &key, &validation).unwrap();
291291
assert_eq!(c.claims.iss, "joe");
292292
}
293+
294+
// Regression tests for type confusion vulnerability where malformed claims
295+
// (eg nbf or exp as strings) were silently treated as "not present" when not required
296+
#[derive(Debug, Serialize)]
297+
struct ClaimsWithStringNbf {
298+
sub: String,
299+
nbf: String, // should be a number
300+
}
301+
302+
#[derive(Debug, Serialize)]
303+
struct ClaimsWithStringExp {
304+
sub: String,
305+
exp: String, // should be a number
306+
}
307+
308+
#[test]
309+
#[wasm_bindgen_test]
310+
fn test_string_nbf_rejected_when_validate_nbf_enabled() {
311+
// Create token with nbf as string (malformed)
312+
let claims = ClaimsWithStringNbf {
313+
sub: "test".to_string(),
314+
nbf: "99999999999".to_string(), // Far future as string
315+
};
316+
let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(b"secret")).unwrap();
317+
318+
let mut validation = Validation::new(Algorithm::HS256);
319+
validation.validate_nbf = true;
320+
validation.required_spec_claims = std::collections::HashSet::new();
321+
322+
let result =
323+
decode::<serde_json::Value>(&token, &DecodingKey::from_secret(b"secret"), &validation);
324+
325+
assert!(result.is_err());
326+
assert_eq!(result.unwrap_err().into_kind(), ErrorKind::InvalidClaimFormat("nbf".to_string()));
327+
}
328+
329+
#[test]
330+
#[wasm_bindgen_test]
331+
fn test_string_exp_rejected_when_validate_exp_enabled() {
332+
// Create token with exp as string (malformed)
333+
let claims = ClaimsWithStringExp {
334+
sub: "test".to_string(),
335+
exp: "99999999999".to_string(), // Far future as string
336+
};
337+
let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(b"secret")).unwrap();
338+
339+
let mut validation = Validation::new(Algorithm::HS256);
340+
validation.validate_exp = true;
341+
validation.required_spec_claims = std::collections::HashSet::new();
342+
343+
let result =
344+
decode::<serde_json::Value>(&token, &DecodingKey::from_secret(b"secret"), &validation);
345+
346+
assert!(result.is_err());
347+
assert_eq!(result.unwrap_err().into_kind(), ErrorKind::InvalidClaimFormat("exp".to_string()));
348+
}

0 commit comments

Comments
 (0)