Skip to content

[Vuln] Uncaught OverflowError / TypeError in jwt.decode() time-claim validation #420

Description

@kaii-k

0x01 Affected version

vendor: https://github.com/mpdavis/python-jose
version: 3.5.0 (latest, verified); earlier releases share the same code path and are very likely affected.

0x02 What kind of vulnerability is it? Who is impacted?

jose.jwt.decode() documents that it raises JWTError / JWTClaimsError / ExpiredSignatureError. When a token's exp, nbf, or iat claim is a JSON value that int() rejects with an exception that is not a subclass of ValueError, that exception propagates uncaught out of decode():

Claim value Uncaught exception
Infinity, -Infinity (parsed by json.loads by default) OverflowError
overflowing float literal, e.g. 1e999 OverflowError
JSON array [1,2] / object {} TypeError

Applications that catch only JWTError (the documented contract) do not catch these, so a crafted token produces an unhandled exception — HTTP 500 / worker crash / an error path that assumes a JWTError — instead of a clean 401. Denial of service; no confidentiality or integrity impact. CWE-248 (Uncaught Exception).

Suggested severity: Low — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L (3.1).

0x03 Root cause

jose/jwt.py:

  • the int(claims["iat"]) call at L269 — inside except ValueError: (L270)
  • the int(claims["nbf"]) call at L294 — inside except ValueError: (L295)
  • the int(claims["exp"]) call at L324 — inside except ValueError: (L325)

OverflowError and TypeError are not subclasses of ValueError, so they are not caught and propagate past decode().

For contrast, the _validate_at_hash() helper in the same file (L467) already catches (TypeError, ValueError) — the three time-claim validators are inconsistent with it.

json.loads() (jose/jwt.py L167, L243) is called without parse_constant, so Infinity / -Infinity / NaN are accepted and turned into Python floats before validation.

0x04 Proof of Concept

from jose import jwt
import base64

h = base64.urlsafe_b64encode(b'{"alg":"HS256"}').rstrip(b'=')
for body in (b'{"exp": Infinity}', b'{"nbf": 1e999}', b'{"iat": Infinity}',
             b'{"exp": [1,2]}', b'{"exp": {}}'):
    p = base64.urlsafe_b64encode(body).rstrip(b'=')
    token = (h + b'.' + p + b'.AAAA').decode()
    try:
        jwt.decode(token, "", options={"verify_signature": False})
    except Exception as e:
        print(f"{body!r:22} -> {type(e).__name__}: {e}")

Output (python-jose[cryptography]==3.5.0, Python 3.13):

b'{"exp": Infinity}' -> OverflowError: cannot convert float infinity to integer
b'{"nbf": 1e999}' -> OverflowError: cannot convert float infinity to integer
b'{"iat": Infinity}' -> OverflowError: cannot convert float infinity to integer
b'{"exp": [1,2]}' -> TypeError: int() argument must be a string, a bytes-like object or a real number, not 'list'
b'{"exp": {}}' -> TypeError: int() argument must be a string, a bytes-like object or a real number, not 'dict'

Reachability:

  1. No signature required — jwt.decode(token, "", options={"verify_signature": False}) is a documented, supported option (inspect claims / verify signature elsewhere). The token is fully attacker-controlled.
  2. Any holder of a validly-signed token — claims validation runs after signature verification. A validly-signed HS256 token carrying {"exp": Infinity} raises the same uncaught OverflowError from jwt.decode(token, secret, algorithms=["HS256"]).

Control: {"exp": "abc"} correctly raises JWTClaimsError: Expiration Time claim (exp) must be an integer., and {"exp": 1000000000} raises ExpiredSignatureError — confirming this is specific to the missed exception types, not a general parsing break.

0x05 Suggested fix

In _validate_exp, _validate_nbf, _validate_iat:

    try:
        exp = int(claims["exp"])
    except (TypeError, ValueError, OverflowError):
        raise JWTClaimsError("Expiration Time claim (exp) must be an integer.")

Optionally also pass a parse_constant callback to the json.loads() calls in jwt.py / jws.py so Infinity / -Infinity / NaN are rejected at parse time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions