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 }