libmicrohttpd

HTTP/1.x server C library (MHD 1.x, stable)
Log | Files | Refs | Submodules | README | LICENSE

fuzz_auth_header.c (18028B)


      1 /*
      2   This file is part of libmicrohttpd
      3   Copyright (C) 2026 Christian Grothoff
      4 
      5   This library is free software; you can redistribute it and/or
      6   modify it under the terms of the GNU Lesser General Public
      7   License as published by the Free Software Foundation; either
      8   version 2.1 of the License, or (at your option) any later version.
      9 
     10   This library is distributed in the hope that it will be useful,
     11   but WITHOUT ANY WARRANTY; without even the implied warranty of
     12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     13   Lesser General Public License for more details.
     14 
     15   You should have received a copy of the GNU Lesser General Public
     16   License along with this library.
     17   If not, see <http://www.gnu.org/licenses/>.
     18 */
     19 
     20 /**
     21  * @file fuzz/fuzz_auth_header.c
     22  * @brief Direct fuzzer for the "Authorization:" header parsers.
     23  * @author Christian Grothoff
     24  *
     25  * MHD_get_rq_dauth_params_() and MHD_get_rq_bauth_params_() are internal
     26  * (they live in gen_auth.c and are not exported), and both need a
     27  * `struct MHD_Connection`.  Rather than pushing bytes through a socket,
     28  * this harness assembles the *minimal* connection object those two
     29  * functions actually touch -- a daemon pointer, a memory pool, a state
     30  * and a single "Authorization" header -- and then varies only the header
     31  * value.  That makes this the fastest way to explore the parameter
     32  * parser (roughly two orders of magnitude more executions per second
     33  * than fuzz_request).
     34  *
     35  * Besides memory-safety (ASAN), the harness checks the parser output for
     36  * internal consistency: every returned parameter must be a sub-range of
     37  * the header value that was fed in.  A parameter pointing outside of it
     38  * would be a parser bug that ASAN alone might not catch.
     39  *
     40  * Input format:
     41  *   byte 0    scheme selector / pool size selector
     42  *   byte 1..  the raw "Authorization" header value
     43  */
     44 
     45 #define FUZZ_HARNESS_NAME "fuzz_auth_header"
     46 #include "fuzz_common.h"
     47 
     48 /* internal.h pulls in MHD_config.h and <microhttpd.h> in the right
     49    order; including <microhttpd.h> first would redefine _MHD_EXTERN. */
     50 #include "internal.h"
     51 #include "memorypool.h"
     52 #include "gen_auth.h"
     53 #include "mhd_str.h"
     54 
     55 #ifdef DAUTH_SUPPORT
     56 #include "digestauth.h"
     57 #endif
     58 #ifdef BAUTH_SUPPORT
     59 #include "basicauth.h"
     60 #endif
     61 
     62 static const size_t pool_sizes[] = { 256, 512, 1024, 4096, 32768 };
     63 
     64 
     65 /**
     66  * Exactly-sized, NUL terminated copy of @a len bytes of @a src.
     67  *
     68  * "Exactly sized" is the point: the allocation is @a len + 1 bytes and
     69  * not one byte more, so ASAN's redzone sits immediately behind the
     70  * terminator and any read past it is reported.
     71  */
     72 static char *
     73 fuzz_dup_n (const char *src,
     74             size_t len)
     75 {
     76   char *r = (char *) malloc (len + 1);
     77 
     78   if (NULL == r)
     79     return NULL;
     80   memcpy (r, src, len);
     81   r[len] = '\0';
     82   return r;
     83 }
     84 
     85 
     86 /**
     87  * gen_auth.c logs through MHD_DLOG(), which dereferences the daemon of
     88  * the connection, so a real (but idle) daemon is required.  It is
     89  * created once and reused for the whole run.
     90  */
     91 static struct MHD_Daemon *shared_daemon;
     92 
     93 /** Statistics, printed at exit with --verbose. */
     94 static unsigned long stat_dauth_parsed;
     95 static unsigned long stat_dauth_failed;
     96 static unsigned long stat_bauth_parsed;
     97 static unsigned long stat_bauth_failed;
     98 
     99 
    100 static void
    101 print_stats (void)
    102 {
    103   if (! fuzz_verbose)
    104     return;
    105   fprintf (stderr,
    106            "%s: digest headers parsed=%lu rejected=%lu; "
    107            "basic headers parsed=%lu rejected=%lu\n",
    108            FUZZ_HARNESS_NAME,
    109            stat_dauth_parsed, stat_dauth_failed,
    110            stat_bauth_parsed, stat_bauth_failed);
    111 }
    112 
    113 
    114 static enum MHD_Result
    115 dummy_ahc (void *cls,
    116            struct MHD_Connection *connection,
    117            const char *url,
    118            const char *method,
    119            const char *version,
    120            const char *upload_data,
    121            size_t *upload_data_size,
    122            void **req_cls)
    123 {
    124   (void) cls; (void) connection; (void) url; (void) method; (void) version;
    125   (void) upload_data; (void) upload_data_size; (void) req_cls;
    126   return MHD_NO;
    127 }
    128 
    129 
    130 static void
    131 stop_shared_daemon (void)
    132 {
    133   if (NULL != shared_daemon)
    134   {
    135     MHD_stop_daemon (shared_daemon);
    136     shared_daemon = NULL;
    137   }
    138 }
    139 
    140 
    141 static struct MHD_Daemon *
    142 get_shared_daemon (void)
    143 {
    144   if (NULL == shared_daemon)
    145   {
    146     shared_daemon =
    147       MHD_start_daemon (MHD_USE_NO_LISTEN_SOCKET
    148                         | (fuzz_verbose ? MHD_USE_ERROR_LOG : 0u),
    149                         0, NULL, NULL, &dummy_ahc, NULL,
    150                         MHD_OPTION_END);
    151     if (NULL != shared_daemon)
    152     {
    153       (void) atexit (&stop_shared_daemon);
    154       (void) atexit (&print_stats);
    155     }
    156   }
    157   return shared_daemon;
    158 }
    159 
    160 
    161 #ifdef DAUTH_SUPPORT
    162 /**
    163  * Every parsed parameter must be a sub-range of the header value.
    164  */
    165 static void
    166 check_param (const struct MHD_RqDAuthParam *pm,
    167              const char *base,
    168              size_t base_len,
    169              const char *what)
    170 {
    171   char msg[256];
    172 
    173   if (NULL == pm->value.str)
    174   {
    175     if (0 != pm->value.len)
    176     {
    177       (void) snprintf (msg, sizeof (msg),
    178                        "digest parameter '%s' has NULL string but "
    179                        "non-zero length", what);
    180       fuzz_report_finding (msg);
    181     }
    182     return;
    183   }
    184   if ( (pm->value.str < base) ||
    185        (pm->value.str > base + base_len) ||
    186        (pm->value.len > base_len) ||
    187        (pm->value.str + pm->value.len > base + base_len) )
    188   {
    189     (void) snprintf (msg, sizeof (msg),
    190                      "digest parameter '%s' points outside of the "
    191                      "Authorization header value", what);
    192     fuzz_report_finding (msg);
    193   }
    194 }
    195 
    196 
    197 #endif /* DAUTH_SUPPORT */
    198 
    199 
    200 int
    201 LLVMFuzzerTestOneInput (const uint8_t *data,
    202                         size_t size)
    203 {
    204   static const char hdr_name[] = MHD_HTTP_HEADER_AUTHORIZATION;
    205   struct MHD_Connection c;
    206   struct MHD_HTTP_Req_Header h;
    207   struct MemoryPool *pool;
    208   char *value;
    209   size_t vlen;
    210   unsigned int sel;
    211   size_t pool_size;
    212 
    213   if (size < 2)
    214     return 0;
    215   if (NULL == get_shared_daemon ())
    216     return 0;
    217   sel = data[0];
    218   vlen = size - 1;
    219   if (vlen > 4096)
    220     vlen = 4096;
    221   pool_size = pool_sizes[(sel >> 3) % (sizeof (pool_sizes)
    222                                        / sizeof (pool_sizes[0]))];
    223 
    224   /* Exactly sized, zero-terminated copy of the header value, so that
    225      ASAN catches any read past its end. */
    226   value = (char *) malloc (vlen + 1);
    227   if (NULL == value)
    228     return 0;
    229   memcpy (value, data + 1, vlen);
    230   value[vlen] = '\0';
    231 
    232   pool = MHD_pool_create (pool_size);
    233   if (NULL == pool)
    234   {
    235     free (value);
    236     return 0;
    237   }
    238 
    239   memset (&c, 0, sizeof (c));
    240   memset (&h, 0, sizeof (h));
    241   h.header = hdr_name;
    242   h.header_size = MHD_STATICSTR_LEN_ (MHD_HTTP_HEADER_AUTHORIZATION);
    243   h.value = value;
    244   h.value_size = vlen;
    245   h.kind = MHD_HEADER_KIND;
    246   c.daemon = shared_daemon;
    247   c.pool = pool;
    248   c.rq.headers_received = &h;
    249   c.rq.headers_received_tail = &h;
    250   c.state = MHD_CONNECTION_HEADERS_PROCESSED;
    251 
    252 #ifdef DAUTH_SUPPORT
    253   if (0 == (sel & 0x01))
    254   {
    255     const struct MHD_RqDAuth *da;
    256 
    257     da = MHD_get_rq_dauth_params_ (&c);
    258     if (NULL == da)
    259       stat_dauth_failed++;
    260     else
    261     {
    262       stat_dauth_parsed++;
    263       check_param (&da->nonce, value, vlen, "nonce");
    264       check_param (&da->opaque, value, vlen, "opaque");
    265       check_param (&da->response, value, vlen, "response");
    266       check_param (&da->username, value, vlen, "username");
    267       check_param (&da->username_ext, value, vlen, "username*");
    268       check_param (&da->realm, value, vlen, "realm");
    269       check_param (&da->uri, value, vlen, "uri");
    270       check_param (&da->qop_raw, value, vlen, "qop");
    271       check_param (&da->cnonce, value, vlen, "cnonce");
    272       check_param (&da->nc, value, vlen, "nc");
    273       /* The result must be stable: a second call returns the cache. */
    274       if (da != MHD_get_rq_dauth_params_ (&c))
    275         fuzz_report_finding ("MHD_get_rq_dauth_params_() is not idempotent");
    276     }
    277   }
    278 #endif /* DAUTH_SUPPORT */
    279 #ifdef BAUTH_SUPPORT
    280   if (0 != (sel & 0x01))
    281   {
    282     const struct MHD_RqBAuth *ba;
    283 
    284     ba = MHD_get_rq_bauth_params_ (&c);
    285     if (NULL == ba)
    286       stat_bauth_failed++;
    287     else
    288     {
    289       stat_bauth_parsed++;
    290       if (NULL != ba->token68.str)
    291       {
    292         if ( (ba->token68.str < value) ||
    293              (ba->token68.str + ba->token68.len > value + vlen) )
    294           fuzz_report_finding ("basic auth token68 points outside of the "
    295                                "Authorization header value");
    296         /* Decoding must fit into the documented maximum size. */
    297         if (0 != ba->token68.len)
    298         {
    299           size_t need = MHD_base64_max_dec_size_ (ba->token68.len);
    300           uint8_t *bin = (uint8_t *) malloc ((0 == need) ? 1 : need);
    301 
    302           if (NULL != bin)
    303           {
    304             size_t r = MHD_base64_to_bin_n (ba->token68.str,
    305                                             ba->token68.len,
    306                                             bin,
    307                                             need);
    308             if (r > need)
    309               fuzz_report_finding ("MHD_base64_to_bin_n() exceeded "
    310                                    "MHD_base64_max_dec_size_()");
    311             free (bin);
    312           }
    313         }
    314       }
    315       if (ba != MHD_get_rq_bauth_params_ (&c))
    316         fuzz_report_finding ("MHD_get_rq_bauth_params_() is not idempotent");
    317     }
    318   }
    319 #endif /* BAUTH_SUPPORT */
    320 
    321 #ifdef DAUTH_SUPPORT
    322   /* The connection-less digest helpers.  They belong here rather than
    323      in fuzz_request because they are pure functions of their string
    324      arguments: this harness reaches roughly two orders of magnitude
    325      more executions per second, and -- more importantly -- it can hand
    326      them a username and realm taken straight from the fuzzer instead of
    327      the fixed constants fuzz_request has to use.
    328 
    329      Every output buffer is a heap allocation of *exactly* the size
    330      declared to MHD, and that size is swept down to zero, so a helper
    331      that writes its full digest into a buffer that is too small is
    332      caught immediately by ASAN's redzone rather than silently
    333      corrupting an adjacent object.  That is precisely the shape of the
    334      overflow fixed in commit 5a73c1ae. */
    335   if (0 != (sel & 0x02))
    336   {
    337     /* Exactly one base hashing algorithm per entry.  In particular
    338        MHD_DIGEST_AUTH_ALGO3_INVALID must not appear: every one of these
    339        helpers routes through digest_get_hash_size(), which asserts that
    340        precisely one of MD5 / SHA-256 / SHA-512-256 is named.  Passing
    341        INVALID is an API violation on the caller's side, not something
    342        worth fuzzing. */
    343     static const enum MHD_DigestAuthAlgo3 algo3s[] = {
    344       MHD_DIGEST_AUTH_ALGO3_MD5,
    345       MHD_DIGEST_AUTH_ALGO3_SHA256,
    346       MHD_DIGEST_AUTH_ALGO3_SHA512_256,
    347       MHD_DIGEST_AUTH_ALGO3_MD5_SESSION,
    348       MHD_DIGEST_AUTH_ALGO3_SHA256_SESSION,
    349       MHD_DIGEST_AUTH_ALGO3_SHA512_256_SESSION
    350     };
    351     enum MHD_DigestAuthAlgo3 a =
    352       algo3s[(sel >> 5) % (sizeof (algo3s) / sizeof (algo3s[0]))];
    353     size_t hs = MHD_digest_get_hash_size (a);
    354 
    355     /* Split the fuzzer-supplied header value into username / realm /
    356        password.  Each gets its OWN exactly-sized allocation rather than
    357        being carved out of `value` in place: that way each string is
    358        followed by its own ASAN redzone, so a helper reading one byte
    359        past the end of the username is reported precisely instead of
    360        quietly running into the realm that would follow it in a shared
    361        buffer. */
    362     size_t o1 = vlen / 3;
    363     size_t o2 = (2 * vlen) / 3;
    364     char *user = fuzz_dup_n (value, o1);
    365     char *realm = fuzz_dup_n (value + o1, o2 - o1);
    366     char *pass = fuzz_dup_n (value + o2, vlen - o2);
    367 
    368     if ( (NULL != user) && (NULL != realm) && (NULL != pass) )
    369     {
    370       if ( (0 != hs) &&
    371            (hs <= 64) )
    372       {
    373         size_t claim = hs - (size_t) (data[0] % (unsigned int) (hs + 1u));
    374         void *bin = malloc (claim);
    375 
    376         if (NULL != bin)
    377         {
    378           (void) MHD_digest_auth_calc_userdigest (a, user, realm, pass,
    379                                                   bin, claim);
    380           (void) MHD_digest_auth_calc_userhash (a, user, realm, bin, claim);
    381           free (bin);
    382         }
    383       }
    384       {
    385         size_t need = (0 != hs) ? (2 * hs + 1) : 1;
    386         size_t claim = need - (size_t) (data[0] % (unsigned int) (need + 1u));
    387         char *hex = (char *) malloc (claim);
    388 
    389         if (NULL != hex)
    390         {
    391           (void) MHD_digest_auth_calc_userhash_hex (a, user, realm,
    392                                                     hex, claim);
    393           free (hex);
    394         }
    395       }
    396     }
    397     free (user);
    398     free (realm);
    399     free (pass);
    400   }
    401 #endif /* DAUTH_SUPPORT */
    402 
    403   MHD_pool_destroy (pool);
    404   free (value);
    405   return 0;
    406 }
    407 
    408 
    409 /* ------------------------------------------------------------------ */
    410 /* Generator                                                           */
    411 /* ------------------------------------------------------------------ */
    412 
    413 static const char *const gen_param_names[] = {
    414   "username", "username*", "realm", "nonce", "uri", "response", "algorithm",
    415   "qop", "nc", "cnonce", "opaque", "userhash", "charset", "domain",
    416   "unknown", "", "USERNAME", "user name"
    417 };
    418 
    419 static const char *const gen_param_values[] = {
    420   "\"user\"", "user", "\"\"", "\"a\\\"b\"", "\"a\\\\\"", "\"\\\"",
    421   "UTF-8''a%20b", "utf-8''%41", "''", "'", "true", "false", "TRUE",
    422   "auth", "auth-int", "auth,auth-int", "\"auth\"", "MD5", "SHA-256",
    423   "SHA-512-256", "BOGUS", "\"SHA-256\"", "00000001", "ffffffff",
    424   "0123456789abcdef0123456789abcdef",
    425   "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    426   "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    427   "\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    428   "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\""
    429 };
    430 
    431 
    432 static size_t
    433 fuzz_generate (struct fuzz_rng *rng,
    434                uint8_t *buf,
    435                size_t cap)
    436 {
    437   size_t len = 0;
    438   unsigned int nparams;
    439   unsigned int i;
    440   int basic;
    441 
    442   if (cap < 32)
    443     return 0;
    444   basic = fuzz_chance (rng, 4);
    445   buf[len++] = (uint8_t) ((fuzz_byte (rng) & 0xFE) | (basic ? 1u : 0u));
    446 
    447 /* NB: evaluate the argument exactly once -- it usually contains a
    448    call into the PRNG. */
    449 #define ADD(s) \
    450   do { \
    451     const char *s_ = (s); \
    452     size_t l_ = strlen (s_); \
    453     if (len + l_ >= cap) \
    454     return len; \
    455     memcpy (buf + len, s_, l_); \
    456     len += l_; \
    457   } while (0)
    458 
    459   if (basic)
    460   {
    461     unsigned int n;
    462 
    463     ADD ("Basic ");
    464     n = fuzz_below (rng, 48);
    465     for (i = 0; (i < n) && (len < cap); i++)
    466       buf[len++] =
    467         (uint8_t) "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    468         "0123456789+/= "[fuzz_below (rng, 66)];
    469     return len;
    470   }
    471 
    472   ADD ("Digest ");
    473   nparams = 1 + fuzz_below (rng, 12);
    474   for (i = 0; i < nparams; i++)
    475   {
    476     if (0 != i)
    477       ADD (fuzz_chance (rng, 8) ? "," : ", ");
    478     ADD (gen_param_names[fuzz_below (rng,
    479                                      (uint32_t) (sizeof (gen_param_names)
    480                                                  / sizeof (char *)))]);
    481     if (! fuzz_chance (rng, 10))
    482       ADD (fuzz_chance (rng, 10) ? " = " : "=");
    483     ADD (gen_param_values[fuzz_below (rng,
    484                                       (uint32_t) (sizeof (gen_param_values)
    485                                                   / sizeof (char *)))]);
    486   }
    487 #undef ADD
    488   return len;
    489 }
    490 
    491 
    492 /* ------------------------------------------------------------------ */
    493 /* Seed corpus                                                         */
    494 /* ------------------------------------------------------------------ */
    495 
    496 struct ah_seed
    497 {
    498   const char *txt;
    499   size_t len;
    500 };
    501 
    502 #define ASEED(t) { t, sizeof (t) - 1 }
    503 
    504 static const struct ah_seed ah_seeds[] = {
    505   ASEED ("\x00" "Digest username=\"user\", realm=\"TestRealm\", "
    506          "nonce=\"0123456789abcdef\", uri=\"/a\", qop=auth, nc=00000001, "
    507          "cnonce=\"x\", algorithm=MD5, response=\"0123456789abcdef\""),
    508   ASEED ("\x00" "Digest algorithm=BOGUS"),
    509   ASEED ("\x00" "Digest algorithm="),
    510   ASEED ("\x00" "Digest username*=UTF-8''a%20b"),
    511   ASEED ("\x00" "Digest username*=''"),
    512   ASEED ("\x00" "Digest username=\"\\\""),
    513   ASEED ("\x00" "Digest userhash=true, username=\"aaaa\""),
    514   ASEED ("\x00" "Digest response=\""
    515          "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    516          "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\""),
    517   ASEED ("\x00" "Digest"),
    518   ASEED ("\x00" "Digest ,,,,,"),
    519   ASEED ("\x00" "Digest nc=\"ffffffffffffffffffffffff\""),
    520   ASEED ("\x01" "Basic dXNlcjpwYXNz"),
    521   ASEED ("\x01" "Basic "),
    522   ASEED ("\x01" "Basic ===="),
    523   ASEED ("\x01" "Basic QQ==QQ=="),
    524 
    525   /* Bit 0x02 of byte 0 additionally runs the connection-less digest
    526      helpers (MHD_digest_auth_calc_userdigest/_userhash/_userhash_hex)
    527      with the header value split into username / realm / password.  Bits
    528      5-7 pick the algorithm and byte 0 also sets the deliberately
    529      undersized output-buffer length, so the seeds below cover several
    530      algorithms at several buffer sizes.  Without a seed here the branch
    531      is only reachable by a lucky bit flip in byte 0.
    532 
    533      The three *_SESSION algorithms are not seeded separately: they hash
    534      with the same primitive as their non-session counterpart, so they
    535      reach no code the entries below do not, and byte 0 is the byte a
    536      mutator flips first anyway. */
    537   ASEED ("\x02" "user:TestRealm:pass"),
    538   ASEED ("\x22" "user:TestRealm:pass"),
    539   ASEED ("\x42" "user:TestRealm:pass"),
    540   ASEED ("\xa2" "user:TestRealm:pass"),
    541   /* Degenerate splits: empty username, empty realm, empty password. */
    542   ASEED ("\x02" "::"),
    543   ASEED ("\x02" ""),
    544   /* Both the parser and the helpers in one execution. */
    545   ASEED ("\x02" "Digest username=\"user\", realm=\"TestRealm\"")
    546 };
    547 
    548 
    549 static size_t
    550 fuzz_seed_count (void)
    551 {
    552   return sizeof (ah_seeds) / sizeof (ah_seeds[0]);
    553 }
    554 
    555 
    556 static const uint8_t *
    557 fuzz_seed_get (size_t idx,
    558                size_t *len)
    559 {
    560   *len = ah_seeds[idx].len;
    561   return (const uint8_t *) ah_seeds[idx].txt;
    562 }