test_basic_auth.c (12691B)
1 /* 2 This file is part of libmicrohttpd 3 Copyright (C) 2026 Christian Grothoff 4 5 libmicrohttpd is free software; you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as published 7 by the Free Software Foundation; either version 2, or (at your 8 option) any later version. 9 10 libmicrohttpd is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with libmicrohttpd; see the file COPYING. If not, write to the 17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 18 Boston, MA 02110-1301, USA. 19 */ 20 21 /** 22 * @file testzzuf/test_basic_auth.c 23 * @brief Fuzzing testcase for Basic Authentication 24 * @author Christian Grothoff 25 * 26 * Before this test existed, no fuzzing test of this directory ever sent an 27 * "Authorization:" header, so neither the Base64 decoder nor the username / 28 * password splitting of MHD were reachable by the fuzzer at all. 29 * 30 * libcurl performs a normal Basic Authentication handshake (so that zzuf's 31 * bit flips land inside a real "Authorization: Basic" header) and the raw 32 * socket client adds hand-crafted headers with Base64 payloads that are 33 * truncated, over-long, contain characters outside the Base64 alphabet, 34 * decode to data with binary zeros or lack the ':' separator entirely. 35 */ 36 37 #include "platform.h" 38 #include <curl/curl.h> 39 #include <microhttpd.h> 40 #include <stdlib.h> 41 #include <string.h> 42 43 #include "mhd_zzuf_common.h" 44 45 #define TEST_MAGIC_MARKER 0xFEE1C0DE 46 47 #define DENIED_PAGE "Access denied." 48 #define GRANTED_PAGE "Access granted." 49 50 /** 51 * The port offset used if the port cannot be auto-detected. 52 */ 53 #define TEST_PORT_OFFSET 165 54 55 #define TEST_REALM "TestRealm" 56 #define TEST_USERNAME "testuser" 57 #define TEST_PASSWORD "testpass" 58 59 #define TEST_URI "/bauth" 60 61 #define HOST_HDR "Host: " ZZUF_MHD_LISTEN_IP "\r\n" 62 63 /** 64 * The size of the buffer used to build the raw requests. 65 */ 66 #define RAW_BUF_SIZE 8192 67 68 69 /** 70 * The closure of the access handler callback. 71 */ 72 struct ahc_param 73 { 74 /** 75 * Must have #TEST_MAGIC_MARKER value. 76 */ 77 unsigned int magic; 78 79 /** 80 * Non-zero if any error has been encountered. 81 */ 82 unsigned int err_flag; 83 84 /** 85 * The checksum over the decoded credentials, only used so that the 86 * compiler cannot optimise the memory accesses away. 87 */ 88 unsigned int cred_sum; 89 }; 90 91 92 static enum MHD_Result 93 ahc_basic (void *cls, 94 struct MHD_Connection *connection, 95 const char *url, 96 const char *method, 97 const char *version, 98 const char *upload_data, 99 size_t *upload_data_size, 100 void **req_cls) 101 { 102 static int marker; 103 struct ahc_param *param = (struct ahc_param *) cls; 104 struct MHD_BasicAuthInfo *creds; 105 struct MHD_Response *response; 106 enum MHD_Result ret; 107 int authenticated = 0; 108 109 (void) url; (void) method; (void) version; (void) upload_data; 110 111 if ((NULL == param) || (TEST_MAGIC_MARKER != param->magic)) 112 { 113 fprintf (stderr, "The 'cls' parameter is invalid " 114 "at line %d.\n", (int) __LINE__); 115 fflush (stderr); 116 abort (); 117 } 118 if (NULL == *req_cls) 119 { 120 *req_cls = ▮ 121 return MHD_YES; 122 } 123 if ((NULL != upload_data_size) && (0 != *upload_data_size)) 124 { 125 *upload_data_size = 0; /* Discard the body */ 126 return MHD_YES; 127 } 128 129 /* This is the call that decodes the (fuzzed or hand-crafted) client's 130 "Authorization: Basic" header. */ 131 creds = MHD_basic_auth_get_username_password3 (connection); 132 if (NULL != creds) 133 { 134 size_t i; 135 136 if (NULL == creds->username) 137 { 138 fprintf (stderr, "The 'username' member is NULL " 139 "at line %d.\n", (int) __LINE__); 140 param->err_flag = 1; 141 } 142 else 143 { 144 /* Touch every byte, including the zero-termination that MHD 145 guarantees. */ 146 for (i = 0; i <= creds->username_len; ++i) 147 param->cred_sum += (unsigned int) (unsigned char) creds->username[i]; 148 } 149 if (NULL == creds->password) 150 { 151 if (0 != creds->password_len) 152 { 153 fprintf (stderr, "The 'password' is NULL while 'password_len' is " 154 "not zero at line %d.\n", (int) __LINE__); 155 param->err_flag = 1; 156 } 157 } 158 else 159 { 160 for (i = 0; i <= creds->password_len; ++i) 161 param->cred_sum += (unsigned int) (unsigned char) creds->password[i]; 162 } 163 if ((NULL != creds->username) && 164 (MHD_STATICSTR_LEN_ (TEST_USERNAME) == creds->username_len) && 165 (0 == memcmp (creds->username, TEST_USERNAME, 166 MHD_STATICSTR_LEN_ (TEST_USERNAME))) && 167 (NULL != creds->password) && 168 (MHD_STATICSTR_LEN_ (TEST_PASSWORD) == creds->password_len) && 169 (0 == memcmp (creds->password, TEST_PASSWORD, 170 MHD_STATICSTR_LEN_ (TEST_PASSWORD)))) 171 authenticated = 1; 172 MHD_free (creds); 173 } 174 175 if (authenticated) 176 { 177 response = 178 MHD_create_response_from_buffer_static (MHD_STATICSTR_LEN_ ( 179 GRANTED_PAGE), 180 GRANTED_PAGE); 181 if (NULL == response) 182 return MHD_NO; /* External error */ 183 if (zzuf_use_close || ! zzuf_oneone) 184 (void) MHD_add_response_header (response, 185 MHD_HTTP_HEADER_CONNECTION, "close"); 186 ret = MHD_queue_response (connection, MHD_HTTP_OK, response); 187 MHD_destroy_response (response); 188 return ret; 189 } 190 191 response = 192 MHD_create_response_from_buffer_static (MHD_STATICSTR_LEN_ (DENIED_PAGE), 193 DENIED_PAGE); 194 if (NULL == response) 195 return MHD_NO; /* External error */ 196 if (zzuf_use_close || ! zzuf_oneone) 197 (void) MHD_add_response_header (response, 198 MHD_HTTP_HEADER_CONNECTION, "close"); 199 ret = MHD_queue_basic_auth_required_response3 (connection, TEST_REALM, 200 MHD_NO, response); 201 MHD_destroy_response (response); 202 return ret; 203 } 204 205 206 /** 207 * Build one hostile "Authorization: Basic" request. 208 * 209 * @param variant the number of the variant to build 210 * @param[out] buf the buffer for the request 211 * @param buf_size the size of @a buf 212 * @return the length of the request, or zero on failure 213 */ 214 static size_t 215 build_hostile_request (unsigned int variant, 216 char *buf, 217 size_t buf_size) 218 { 219 static const char b64_alphabet[] = 220 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 221 char big[4096]; 222 const char *value; 223 size_t i; 224 int len; 225 226 switch (variant % 10) 227 { 228 case 0: 229 /* A truncated Base64 payload (not a multiple of four). */ 230 value = "dGVzdHVzZXI"; 231 break; 232 case 1: 233 /* Characters outside the Base64 alphabet. */ 234 value = "!!!!\?\?\?\?===="; /* '?' escaped to avoid a trigraph */ 235 break; 236 case 2: 237 /* Valid Base64 that decodes to a string without ':' separator. */ 238 value = "dGVzdHVzZXI="; 239 break; 240 case 3: 241 /* Valid Base64 that decodes to "\0:\0" (binary zeros). */ 242 value = "ADoA"; 243 break; 244 case 4: 245 /* Valid Base64 that decodes to ":" only (empty username). */ 246 value = "Og=="; 247 break; 248 case 5: 249 /* Padding in the middle of the payload. */ 250 value = "dGVz=dXNlcjpwYXNz"; 251 break; 252 case 6: 253 /* An empty value. */ 254 value = ""; 255 break; 256 case 7: 257 /* A very long, but valid, Base64 payload. */ 258 for (i = 0; i + 1 < sizeof(big); ++i) 259 big[i] = b64_alphabet[i % 64]; 260 big[sizeof(big) - 1] = 0; 261 value = big; 262 break; 263 case 8: 264 /* Whitespace inside the payload. */ 265 value = "dGVz dXNl cjpw YXNz"; 266 break; 267 case 9: 268 default: 269 /* An unknown scheme, and a second "Authorization" header. */ 270 len = snprintf (buf, buf_size, 271 "GET " TEST_URI " HTTP/1.1\r\n" HOST_HDR 272 "Authorization: Bearer dGVzdHVzZXI6dGVzdHBhc3M=\r\n" 273 "Authorization: Basic\r\n" 274 "Connection: close\r\n\r\n"); 275 if ((0 > len) || (buf_size <= (size_t) len)) 276 return 0; 277 return (size_t) len; 278 } 279 len = snprintf (buf, buf_size, 280 "GET " TEST_URI " HTTP/1.1\r\n" HOST_HDR 281 "Authorization: Basic %s\r\n" 282 "Connection: close\r\n\r\n", 283 value); 284 if ((0 > len) || (buf_size <= (size_t) len)) 285 return 0; 286 return (size_t) len; 287 } 288 289 290 static unsigned int 291 client_run (struct MHD_Daemon *d_extern, 292 uint16_t port, 293 void *cls) 294 { 295 struct ahc_param *param = (struct ahc_param *) cls; 296 struct zzuf_curl_driver drv; 297 struct zzuf_curl_sink sink; 298 char *req; 299 unsigned int i; 300 unsigned int loops; 301 302 loops = zzuf_loop_count (); 303 if (! zzuf_curl_driver_init (&drv, d_extern)) 304 return 99; /* Not an MHD error */ 305 req = malloc (RAW_BUF_SIZE); 306 if (NULL == req) 307 { 308 zzuf_curl_driver_deinit (&drv); 309 return 99; /* Not an MHD error */ 310 } 311 312 for (i = 0; i < loops; ++i) 313 { 314 CURL *c; 315 struct zzuf_raw_part part; 316 size_t req_len; 317 318 fprintf (stderr, "."); 319 /* 1. A well-formed Basic Authentication handshake by libcurl, going 320 through the fuzzing layer. */ 321 c = zzuf_curl_setup (port, TEST_URI, &sink); 322 if (NULL == c) 323 { 324 free (req); 325 zzuf_curl_driver_deinit (&drv); 326 return 99; /* Not an MHD error */ 327 } 328 if ((CURLE_OK != curl_easy_setopt (c, CURLOPT_HTTPAUTH, 329 (long) CURLAUTH_BASIC)) || 330 (CURLE_OK != curl_easy_setopt (c, CURLOPT_USERPWD, 331 TEST_USERNAME ":" TEST_PASSWORD))) 332 { 333 curl_easy_cleanup (c); 334 free (req); 335 zzuf_curl_driver_deinit (&drv); 336 return 99; /* Not an MHD error */ 337 } 338 zzuf_curl_driver_perform (&drv, c); 339 curl_easy_cleanup (c); 340 341 /* 2. The hand-crafted hostile headers. */ 342 req_len = build_hostile_request (i, req, RAW_BUF_SIZE); 343 if (0 != req_len) 344 { 345 part.data = req; 346 part.size = req_len; 347 /* Once through the fuzzing layer ... */ 348 if (ZZUF_RAW_SETUP_FAILED == 349 zzuf_raw_exchange (d_extern, port, &part, 1, 350 (unsigned int) ZZUF_CLIENT_TIMEOUT)) 351 { 352 free (req); 353 zzuf_curl_driver_deinit (&drv); 354 return 99; /* Not an MHD error */ 355 } 356 /* ... and once verbatim. */ 357 if (ZZUF_RAW_SETUP_FAILED == 358 zzuf_raw_exchange2 (d_extern, port, 1, &part, 1, 359 (unsigned int) ZZUF_CLIENT_TIMEOUT, 360 NULL, 0, NULL)) 361 { 362 free (req); 363 zzuf_curl_driver_deinit (&drv); 364 return 99; /* Not an MHD error */ 365 } 366 } 367 fflush (stderr); 368 } 369 free (req); 370 zzuf_curl_driver_deinit (&drv); 371 372 if (0 != param->err_flag) 373 { 374 fprintf (stderr, "One or more errors have been detected by the access " 375 "handler callback function. At line %d.\n", (int) __LINE__); 376 return 1; 377 } 378 return 0; 379 } 380 381 382 int 383 main (int argc, char *const *argv) 384 { 385 struct ahc_param param; 386 struct zzuf_run_params rp; 387 unsigned int res; 388 int use_magic_exit_codes; 389 390 zzuf_parse_common_args (argc, argv); 391 use_magic_exit_codes = zzuf_run_with_socat || zzuf_dry_run; 392 393 if (MHD_NO == MHD_is_feature_supported (MHD_FEATURE_BASIC_AUTH)) 394 { 395 fprintf (stderr, "Basic Authentication is not supported by this " 396 "MHD build.\n"); 397 return use_magic_exit_codes ? 77 : 0; 398 } 399 res = zzuf_check_runnable (); 400 if (0 != res) 401 return use_magic_exit_codes ? (int) res : 0; 402 403 if (CURLE_OK != curl_global_init (CURL_GLOBAL_WIN32)) 404 { 405 fprintf (stderr, "curl_global_init() failed at line %d.\n", 406 (int) __LINE__); 407 return use_magic_exit_codes ? 99 : 0; 408 } 409 410 param.magic = (unsigned int) TEST_MAGIC_MARKER; 411 param.err_flag = 0; 412 param.cred_sum = 0; 413 414 memset (&rp, 0, sizeof(rp)); 415 rp.port = zzuf_pick_port (TEST_PORT_OFFSET); 416 rp.ahc = &ahc_basic; 417 rp.ahc_cls = ¶m; 418 rp.rcc = NULL; 419 rp.rcc_cls = NULL; 420 /* The hostile headers are large, use a memory limit that can hold them. */ 421 rp.mem_limit_override = 8192; 422 rp.sweep_profiles = 1; 423 rp.profile_start = 0; 424 rp.client = &client_run; 425 rp.client_cls = ¶m; 426 rp.extra_opts = NULL; 427 428 res = zzuf_run_polling_modes (&rp); 429 curl_global_cleanup (); 430 431 if (99 == res) 432 return use_magic_exit_codes ? 99 : 0; 433 if (77 == res) 434 return use_magic_exit_codes ? 77 : 0; 435 return (0 == res) ? 0 : 1; /* 0 == pass */ 436 }