Coverage Report

Created: 2026-08-31 07:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mod_auth_openidc/test/fuzz/fuzz_jwt.c
Line
Count
Source
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one or more
3
 * contributor license agreements.  Licensed under the Apache License,
4
 * Version 2.0 (the "License"); you may not use this file except in
5
 * compliance with the License.  You may obtain a copy of the License at
6
 *
7
 *   http://www.apache.org/licenses/LICENSE-2.0
8
 *
9
 * Copyright (C) 2017-2026 ZmartZone Holding BV - hans.zandbelt@openidc.com
10
 *
11
 * Fuzz target for oidc_jwt_parse() + oidc_jwt_verify(): parsing of
12
 * attacker-controlled compact JWT/JWS/JWE serializations, followed by
13
 * signature verification against a fixed key set. The key set is the one the
14
 * unit tests carry for their RFC vectors, so the seeds in corpus/jwt that are
15
 * real tokens verify successfully and the success paths past the signature
16
 * check -- claim extraction, re-serialization -- stay reachable (the two JWE
17
 * vectors decrypt, but carry a plain-text rather than a JSON payload, so they
18
 * stop at the payload decode; they still drive the key-unwrap and AEAD code):
19
 *
20
 *   oct   HS256  draft-ietf-oauth-json-web-token-20 §3.1 (no kid)
21
 *   EC    ES256  kid "f6qtj"
22
 *   RSA   RS256  the x5t-headed id_token vector (no kid)
23
 *   RSA   RSA-OAEP private key, RFC 7516 §A.1 (JWE decryption)
24
 *   oct   A128KW, draft-ietf-jose-json-web-encryption-40 §A.3 (JWE decryption)
25
 *
26
 * A token without a kid is tried against every key of a compatible type, so
27
 * mutations of the seeds keep hitting the verification code rather than only
28
 * the kid lookup.
29
 */
30
31
#include "fuzz.h"
32
/* util.h first: see the include-order note in fuzz_metadata.c */
33
/* clang-format off */
34
#include "util.h"      /* test fixture */
35
#include "json.h"
36
#include "jose.h"
37
#include "util/util.h" /* oidc_json_decode_object */
38
/* clang-format on */
39
40
#include <apr_hash.h>
41
#include <apr_pools.h>
42
#include <apr_strings.h>
43
#include <stdlib.h>
44
45
static int g_ready = 0;
46
static apr_hash_t *g_keys = NULL;
47
48
/*
49
 * LeakSanitizer anchors for the key objects.
50
 *
51
 * The JWKs are pool-allocated and point at cjose/OpenSSL objects on the heap.
52
 * APR allocates its pool nodes with mmap() on the distributions the sanitizer
53
 * builds run on, and LSan does not scan anonymous mappings for pointers, so
54
 * from its point of view those heap objects are referenced by nothing. That is
55
 * harmless until libFuzzer runs its mid-run leak check -- which it does after
56
 * any input whose malloc count exceeded its free count, and an RSA verify
57
 * does exactly that the first time it caches a Montgomery context on the key
58
 * -- and then every key, and everything lazily attached to it, is reported as
59
 * a leak and the run stops. Keeping the heap handles in a global (a root LSan
60
 * does scan) makes the whole key graph reachable again; the keys are released
61
 * from an atexit handler so the end-of-run check stays clean as well.
62
 */
63
28
#define FUZZ_JWT_MAX_KEYS 8
64
static void *g_cjose_keys[FUZZ_JWT_MAX_KEYS];
65
66
2
static void fuzz_jwt_keys_destroy(void) {
67
2
  if (g_keys != NULL)
68
2
    oidc_jwk_list_destroy_hash(g_keys);
69
2
  g_keys = NULL;
70
18
  for (int i = 0; i < FUZZ_JWT_MAX_KEYS; i++)
71
16
    g_cjose_keys[i] = NULL;
72
2
}
73
74
static const struct {
75
  const char *name; /* hash key: the kid where the token carries one, descriptive otherwise */
76
  const char *json;
77
} fuzz_jwt_keys[] = {
78
    {"hs256", "{\"kty\":\"oct\",\"k\":\"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-"
79
        "1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow\"}"},
80
    {"f6qtj", "{\"kty\":\"EC\",\"kid\":\"f6qtj\",\"use\":\"sig\","
81
        "\"x\":\"iARwFlN3B3xa8Zn_O-CVfqry68tXIhO9DckKo1yrNg0\","
82
        "\"y\":\"583S_mPS7YVZtLCjx2O69G_JzQPnMxjieOli-9cc_6Q\",\"crv\":\"P-256\"}"},
83
    {"rs256", "{\"kty\":\"RSA\",\"e\":\"AQAB\","
84
        "\"n\":\"3lDyn_ZvG32Pw5kYbRuVxHsPfe9Xt8s9vOXnt8z7_T-hZZvealNhCxz9VEwTJ7TsZ9CLi5c30FjoEJYFkKdd"
85
        "LAdxKo0oOXWc_AWrQvPwht9a-o6dX2fL_9CmXW1hGHXMH0qiLMrFqMSzZeh-GUY6F1woE_eKsAo6LOhP8X77FlEQT2Eu"
86
        "71wu8KC4B3sH_9QTco50KNw14-bRY5j2V2TZelvsXJnvrN4lXtEVYWFkREKeXzMH8DhDyZzh0NcHa7dFBa7rDusyfIHj"
87
        "uP6uAju_Ao6hhdOGjlKePMVtfusWBAI7MWDChLTqiCTvlZnCpkpTTh5m-i7TbE1TwmdbLceq1w\"}"},
88
    {"rsa-oaep",
89
     "{\"kty\":\"RSA\","
90
     "\"n\":\"oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUWcJoZmds2h7M70imEVhRU5djINXtqllXI4D"
91
     "FqcI1DgjT9LewND8MW2Krf3Spsk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2asbOenSZeyaxziK72Uw"
92
     "xrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMStPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2djYg"
93
     "yD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw\","
94
     "\"e\":\"AQAB\","
95
     "\"d\":\"kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5NWV5KntaEeXS1j82E375xxhWMHXyvjYecPT"
96
     "9fpwR_M9gV8n9Hrh2anTpTD93Dt62ypW3yDsJzBnTnrYu1iwWRgBKrEYY46qAZIrA2xAwnm2X7uGR1hghkqDp0Vqj3kbSCz1XyfC"
97
     "s6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vlt3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSndVT"
98
     "IznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ\","
99
     "\"p\":\"1r52Xk46c-LsfB5P442p7atdPUrxQSy4mti_tZI3Mgf2EuFVbUoDBvaRQ-SWxkbkmoEzL7JXroSBjSrK3YIQgYdMgyAEPT"
100
     "PjXv_hI2_1eTSPVZfzL0lffNn03IXqWF5MDFuoUYE0hzb2vhrlN_rKrbfDIwUbTrjjgieRbwC6Cl0\","
101
     "\"q\":\"wLb35x7hmQWZsWJmB_vle87ihgZ19S8lBEROLIsZG4ayZVe9Hi9gDVCOBmUDdaDYVTSNx_8Fyw1YYa9XGrGnDew00J28cR"
102
     "UoeBB_jKI1oma0Orv1T9aXIWxKwd4gvxFImOWr3QRL9KEBRzk2RatUBnmDZJTIAfwTs0g68UZHvtc\","
103
     "\"dp\":\"ZK-YwE7diUh0qR1tR7w8WHtolDx3MZ_OTowiFvgfeQ3SiresXjm9gZ5KLhMXvo-uz-KUJWDxS5pFQ_M0evdo1dKiRTjV"
104
     "w_x4NyqyXPM5nULPkcpU827rnpZzAJKpdhWAgqrXGKAECQH0Xt4taznjnd_zVpAmZZq60WPMBMfKcuE\","
105
     "\"dq\":\"Dq0gfgJ1DdFGXiLvQEZnuKEN0UUmsJBxkjydc3j4ZYdBiMRAy86x0vHCjywcMlYYg4yoC4YZa9hNVcsjqA3FeiL19rk8"
106
     "g6Qn29Tt0cj8qqyFpz9vNDBUfCAiJVeESOjJDZPYHdHY8v1b-o-Z2X5tvLx-TCekf7oxyeKDUqKWjis\","
107
     "\"qi\":\"VIMpMYbPf47dT1w_zDUXfPimsSegnMOA1zTaX7aGk_8urY6R8-ZW1FxU7AlWAyLWybqq6t16VFd7hQd0y6flUK4SlOyd"
108
     "B61gwanOsXGOAOv82cHq0E3eL4HrtZkUuKvnPrMnsUUFlfUdybVzxyjz9JF_XyaY14ardLSjf4L_FNY\"}"},
109
    {"a128kw", "{\"kty\":\"oct\",\"k\":\"GawgguFyGrWKav7AX4VKUg\"}"},
110
    {NULL, NULL},
111
};
112
113
/* engine-called one-time init, pre-forkserver on AFL++: see fuzz.h */
114
2
int LLVMFuzzerInitialize(int *argc, char ***argv) {
115
2
  (void)argc;
116
2
  (void)argv;
117
2
  if (!g_ready) {
118
2
    oidc_test_setup();
119
    /* the keys live for the whole run, in the fixture's pool */
120
2
    apr_pool_t *pool = oidc_test_pool_get();
121
2
    request_rec *r = oidc_test_request_get();
122
2
    g_keys = apr_hash_make(pool);
123
12
    for (int i = 0; (fuzz_jwt_keys[i].name != NULL) && (i < FUZZ_JWT_MAX_KEYS); i++) {
124
10
      oidc_json_t *json = NULL;
125
10
      oidc_jwk_t *jwk = NULL;
126
10
      oidc_jose_error_t err;
127
10
      if ((oidc_json_decode_object(r, fuzz_jwt_keys[i].json, &json) == TRUE) && (json != NULL)) {
128
10
        if ((oidc_jwk_parse_json(pool, json, &jwk, &err) == TRUE) && (jwk != NULL)) {
129
10
          apr_hash_set(g_keys, fuzz_jwt_keys[i].name, APR_HASH_KEY_STRING, jwk);
130
10
          g_cjose_keys[i] = jwk->cjose_jwk;
131
10
        }
132
10
        oidc_json_decref(json);
133
10
      }
134
10
    }
135
2
    atexit(fuzz_jwt_keys_destroy);
136
2
    g_ready = 1;
137
2
  }
138
2
  return 0;
139
2
}
140
141
6.00k
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
142
6.00k
  if (!g_ready)
143
0
    LLVMFuzzerInitialize(NULL, NULL);
144
145
6.00k
  apr_pool_t *pool = NULL;
146
6.00k
  apr_pool_create(&pool, oidc_test_pool_get());
147
148
6.00k
  char *s = apr_pstrmemdup(pool, (const char *)data, size);
149
6.00k
  oidc_jwt_t *jwt = NULL;
150
6.00k
  oidc_jose_error_t err;
151
152
  /* parse (and decrypt, when the input is a JWE for one of the keys) ... */
153
6.00k
  if ((oidc_jwt_parse(pool, s, &jwt, g_keys, FALSE, &err) == TRUE) && (jwt != NULL)) {
154
    /* ... then verify the signature against the key set, and run the claim
155
     * accessors and the serializer over the parsed token either way */
156
232
    oidc_jwt_verify(pool, jwt, g_keys, &err);
157
232
    oidc_jwt_hdr_get(jwt, "typ");
158
232
    oidc_jwt_hdr_get(jwt, "cty");
159
232
    char *value = NULL;
160
232
    oidc_jose_get_string(pool, jwt->payload.value.json, "aud", FALSE, &value, &err);
161
232
    oidc_jose_get_string(pool, jwt->payload.value.json, "nonce", FALSE, &value, &err);
162
232
    double ts = 0;
163
232
    oidc_jose_get_timestamp(jwt->payload.value.json, "nbf", FALSE, &ts, &err);
164
232
    oidc_jose_jwt_serialize(pool, jwt, &err);
165
232
  }
166
  /* a JWT wraps cjose/OpenSSL objects allocated outside the pool */
167
6.00k
  if (jwt != NULL)
168
232
    oidc_jwt_destroy(jwt);
169
170
6.00k
  apr_pool_destroy(pool);
171
6.00k
  return 0;
172
6.00k
}