mirror of
https://git.gnunet.org/libmicrohttpd.git
synced 2026-09-25 04:09:31 +03:00
563 lines
18 KiB
C
563 lines
18 KiB
C
/*
|
|
This file is part of libmicrohttpd
|
|
Copyright (C) 2026 Christian Grothoff
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2.1 of the License, or (at your option) any later version.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with this library.
|
|
If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/**
|
|
* @file fuzz/fuzz_auth_header.c
|
|
* @brief Direct fuzzer for the "Authorization:" header parsers.
|
|
* @author Christian Grothoff
|
|
*
|
|
* MHD_get_rq_dauth_params_() and MHD_get_rq_bauth_params_() are internal
|
|
* (they live in gen_auth.c and are not exported), and both need a
|
|
* `struct MHD_Connection`. Rather than pushing bytes through a socket,
|
|
* this harness assembles the *minimal* connection object those two
|
|
* functions actually touch -- a daemon pointer, a memory pool, a state
|
|
* and a single "Authorization" header -- and then varies only the header
|
|
* value. That makes this the fastest way to explore the parameter
|
|
* parser (roughly two orders of magnitude more executions per second
|
|
* than fuzz_request).
|
|
*
|
|
* Besides memory-safety (ASAN), the harness checks the parser output for
|
|
* internal consistency: every returned parameter must be a sub-range of
|
|
* the header value that was fed in. A parameter pointing outside of it
|
|
* would be a parser bug that ASAN alone might not catch.
|
|
*
|
|
* Input format:
|
|
* byte 0 scheme selector / pool size selector
|
|
* byte 1.. the raw "Authorization" header value
|
|
*/
|
|
|
|
#define FUZZ_HARNESS_NAME "fuzz_auth_header"
|
|
#include "fuzz_common.h"
|
|
|
|
/* internal.h pulls in MHD_config.h and <microhttpd.h> in the right
|
|
order; including <microhttpd.h> first would redefine _MHD_EXTERN. */
|
|
#include "internal.h"
|
|
#include "memorypool.h"
|
|
#include "gen_auth.h"
|
|
#include "mhd_str.h"
|
|
|
|
#ifdef DAUTH_SUPPORT
|
|
#include "digestauth.h"
|
|
#endif
|
|
#ifdef BAUTH_SUPPORT
|
|
#include "basicauth.h"
|
|
#endif
|
|
|
|
static const size_t pool_sizes[] = { 256, 512, 1024, 4096, 32768 };
|
|
|
|
|
|
/**
|
|
* Exactly-sized, NUL terminated copy of @a len bytes of @a src.
|
|
*
|
|
* "Exactly sized" is the point: the allocation is @a len + 1 bytes and
|
|
* not one byte more, so ASAN's redzone sits immediately behind the
|
|
* terminator and any read past it is reported.
|
|
*/
|
|
static char *
|
|
fuzz_dup_n (const char *src,
|
|
size_t len)
|
|
{
|
|
char *r = (char *) malloc (len + 1);
|
|
|
|
if (NULL == r)
|
|
return NULL;
|
|
memcpy (r, src, len);
|
|
r[len] = '\0';
|
|
return r;
|
|
}
|
|
|
|
|
|
/**
|
|
* gen_auth.c logs through MHD_DLOG(), which dereferences the daemon of
|
|
* the connection, so a real (but idle) daemon is required. It is
|
|
* created once and reused for the whole run.
|
|
*/
|
|
static struct MHD_Daemon *shared_daemon;
|
|
|
|
/** Statistics, printed at exit with --verbose. */
|
|
static unsigned long stat_dauth_parsed;
|
|
static unsigned long stat_dauth_failed;
|
|
static unsigned long stat_bauth_parsed;
|
|
static unsigned long stat_bauth_failed;
|
|
|
|
|
|
static void
|
|
print_stats (void)
|
|
{
|
|
if (! fuzz_verbose)
|
|
return;
|
|
fprintf (stderr,
|
|
"%s: digest headers parsed=%lu rejected=%lu; "
|
|
"basic headers parsed=%lu rejected=%lu\n",
|
|
FUZZ_HARNESS_NAME,
|
|
stat_dauth_parsed, stat_dauth_failed,
|
|
stat_bauth_parsed, stat_bauth_failed);
|
|
}
|
|
|
|
|
|
static enum MHD_Result
|
|
dummy_ahc (void *cls,
|
|
struct MHD_Connection *connection,
|
|
const char *url,
|
|
const char *method,
|
|
const char *version,
|
|
const char *upload_data,
|
|
size_t *upload_data_size,
|
|
void **req_cls)
|
|
{
|
|
(void) cls; (void) connection; (void) url; (void) method; (void) version;
|
|
(void) upload_data; (void) upload_data_size; (void) req_cls;
|
|
return MHD_NO;
|
|
}
|
|
|
|
|
|
static void
|
|
stop_shared_daemon (void)
|
|
{
|
|
if (NULL != shared_daemon)
|
|
{
|
|
MHD_stop_daemon (shared_daemon);
|
|
shared_daemon = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
static struct MHD_Daemon *
|
|
get_shared_daemon (void)
|
|
{
|
|
if (NULL == shared_daemon)
|
|
{
|
|
shared_daemon =
|
|
MHD_start_daemon (MHD_USE_NO_LISTEN_SOCKET
|
|
| (fuzz_verbose ? MHD_USE_ERROR_LOG : 0u),
|
|
0, NULL, NULL, &dummy_ahc, NULL,
|
|
MHD_OPTION_END);
|
|
if (NULL != shared_daemon)
|
|
{
|
|
(void) atexit (&stop_shared_daemon);
|
|
(void) atexit (&print_stats);
|
|
}
|
|
}
|
|
return shared_daemon;
|
|
}
|
|
|
|
|
|
#ifdef DAUTH_SUPPORT
|
|
/**
|
|
* Every parsed parameter must be a sub-range of the header value.
|
|
*/
|
|
static void
|
|
check_param (const struct MHD_RqDAuthParam *pm,
|
|
const char *base,
|
|
size_t base_len,
|
|
const char *what)
|
|
{
|
|
char msg[256];
|
|
|
|
if (NULL == pm->value.str)
|
|
{
|
|
if (0 != pm->value.len)
|
|
{
|
|
(void) snprintf (msg, sizeof (msg),
|
|
"digest parameter '%s' has NULL string but "
|
|
"non-zero length", what);
|
|
fuzz_report_finding (msg);
|
|
}
|
|
return;
|
|
}
|
|
if ( (pm->value.str < base) ||
|
|
(pm->value.str > base + base_len) ||
|
|
(pm->value.len > base_len) ||
|
|
(pm->value.str + pm->value.len > base + base_len) )
|
|
{
|
|
(void) snprintf (msg, sizeof (msg),
|
|
"digest parameter '%s' points outside of the "
|
|
"Authorization header value", what);
|
|
fuzz_report_finding (msg);
|
|
}
|
|
}
|
|
|
|
|
|
#endif /* DAUTH_SUPPORT */
|
|
|
|
|
|
int
|
|
LLVMFuzzerTestOneInput (const uint8_t *data,
|
|
size_t size)
|
|
{
|
|
static const char hdr_name[] = MHD_HTTP_HEADER_AUTHORIZATION;
|
|
struct MHD_Connection c;
|
|
struct MHD_HTTP_Req_Header h;
|
|
struct MemoryPool *pool;
|
|
char *value;
|
|
size_t vlen;
|
|
unsigned int sel;
|
|
size_t pool_size;
|
|
|
|
if (size < 2)
|
|
return 0;
|
|
if (NULL == get_shared_daemon ())
|
|
return 0;
|
|
sel = data[0];
|
|
vlen = size - 1;
|
|
if (vlen > 4096)
|
|
vlen = 4096;
|
|
pool_size = pool_sizes[(sel >> 3) % (sizeof (pool_sizes)
|
|
/ sizeof (pool_sizes[0]))];
|
|
|
|
/* Exactly sized, zero-terminated copy of the header value, so that
|
|
ASAN catches any read past its end. */
|
|
value = (char *) malloc (vlen + 1);
|
|
if (NULL == value)
|
|
return 0;
|
|
memcpy (value, data + 1, vlen);
|
|
value[vlen] = '\0';
|
|
|
|
pool = MHD_pool_create (pool_size);
|
|
if (NULL == pool)
|
|
{
|
|
free (value);
|
|
return 0;
|
|
}
|
|
|
|
memset (&c, 0, sizeof (c));
|
|
memset (&h, 0, sizeof (h));
|
|
h.header = hdr_name;
|
|
h.header_size = MHD_STATICSTR_LEN_ (MHD_HTTP_HEADER_AUTHORIZATION);
|
|
h.value = value;
|
|
h.value_size = vlen;
|
|
h.kind = MHD_HEADER_KIND;
|
|
c.daemon = shared_daemon;
|
|
c.pool = pool;
|
|
c.rq.headers_received = &h;
|
|
c.rq.headers_received_tail = &h;
|
|
c.state = MHD_CONNECTION_HEADERS_PROCESSED;
|
|
|
|
#ifdef DAUTH_SUPPORT
|
|
if (0 == (sel & 0x01))
|
|
{
|
|
const struct MHD_RqDAuth *da;
|
|
|
|
da = MHD_get_rq_dauth_params_ (&c);
|
|
if (NULL == da)
|
|
stat_dauth_failed++;
|
|
else
|
|
{
|
|
stat_dauth_parsed++;
|
|
check_param (&da->nonce, value, vlen, "nonce");
|
|
check_param (&da->opaque, value, vlen, "opaque");
|
|
check_param (&da->response, value, vlen, "response");
|
|
check_param (&da->username, value, vlen, "username");
|
|
check_param (&da->username_ext, value, vlen, "username*");
|
|
check_param (&da->realm, value, vlen, "realm");
|
|
check_param (&da->uri, value, vlen, "uri");
|
|
check_param (&da->qop_raw, value, vlen, "qop");
|
|
check_param (&da->cnonce, value, vlen, "cnonce");
|
|
check_param (&da->nc, value, vlen, "nc");
|
|
/* The result must be stable: a second call returns the cache. */
|
|
if (da != MHD_get_rq_dauth_params_ (&c))
|
|
fuzz_report_finding ("MHD_get_rq_dauth_params_() is not idempotent");
|
|
}
|
|
}
|
|
#endif /* DAUTH_SUPPORT */
|
|
#ifdef BAUTH_SUPPORT
|
|
if (0 != (sel & 0x01))
|
|
{
|
|
const struct MHD_RqBAuth *ba;
|
|
|
|
ba = MHD_get_rq_bauth_params_ (&c);
|
|
if (NULL == ba)
|
|
stat_bauth_failed++;
|
|
else
|
|
{
|
|
stat_bauth_parsed++;
|
|
if (NULL != ba->token68.str)
|
|
{
|
|
if ( (ba->token68.str < value) ||
|
|
(ba->token68.str + ba->token68.len > value + vlen) )
|
|
fuzz_report_finding ("basic auth token68 points outside of the "
|
|
"Authorization header value");
|
|
/* Decoding must fit into the documented maximum size. */
|
|
if (0 != ba->token68.len)
|
|
{
|
|
size_t need = MHD_base64_max_dec_size_ (ba->token68.len);
|
|
uint8_t *bin = (uint8_t *) malloc ((0 == need) ? 1 : need);
|
|
|
|
if (NULL != bin)
|
|
{
|
|
size_t r = MHD_base64_to_bin_n (ba->token68.str,
|
|
ba->token68.len,
|
|
bin,
|
|
need);
|
|
if (r > need)
|
|
fuzz_report_finding ("MHD_base64_to_bin_n() exceeded "
|
|
"MHD_base64_max_dec_size_()");
|
|
free (bin);
|
|
}
|
|
}
|
|
}
|
|
if (ba != MHD_get_rq_bauth_params_ (&c))
|
|
fuzz_report_finding ("MHD_get_rq_bauth_params_() is not idempotent");
|
|
}
|
|
}
|
|
#endif /* BAUTH_SUPPORT */
|
|
|
|
#ifdef DAUTH_SUPPORT
|
|
/* The connection-less digest helpers. They belong here rather than
|
|
in fuzz_request because they are pure functions of their string
|
|
arguments: this harness reaches roughly two orders of magnitude
|
|
more executions per second, and -- more importantly -- it can hand
|
|
them a username and realm taken straight from the fuzzer instead of
|
|
the fixed constants fuzz_request has to use.
|
|
|
|
Every output buffer is a heap allocation of *exactly* the size
|
|
declared to MHD, and that size is swept down to zero, so a helper
|
|
that writes its full digest into a buffer that is too small is
|
|
caught immediately by ASAN's redzone rather than silently
|
|
corrupting an adjacent object. That is precisely the shape of the
|
|
overflow fixed in commit 5a73c1ae. */
|
|
if (0 != (sel & 0x02))
|
|
{
|
|
/* Exactly one base hashing algorithm per entry. In particular
|
|
MHD_DIGEST_AUTH_ALGO3_INVALID must not appear: every one of these
|
|
helpers routes through digest_get_hash_size(), which asserts that
|
|
precisely one of MD5 / SHA-256 / SHA-512-256 is named. Passing
|
|
INVALID is an API violation on the caller's side, not something
|
|
worth fuzzing. */
|
|
static const enum MHD_DigestAuthAlgo3 algo3s[] = {
|
|
MHD_DIGEST_AUTH_ALGO3_MD5,
|
|
MHD_DIGEST_AUTH_ALGO3_SHA256,
|
|
MHD_DIGEST_AUTH_ALGO3_SHA512_256,
|
|
MHD_DIGEST_AUTH_ALGO3_MD5_SESSION,
|
|
MHD_DIGEST_AUTH_ALGO3_SHA256_SESSION,
|
|
MHD_DIGEST_AUTH_ALGO3_SHA512_256_SESSION
|
|
};
|
|
enum MHD_DigestAuthAlgo3 a =
|
|
algo3s[(sel >> 5) % (sizeof (algo3s) / sizeof (algo3s[0]))];
|
|
size_t hs = MHD_digest_get_hash_size (a);
|
|
|
|
/* Split the fuzzer-supplied header value into username / realm /
|
|
password. Each gets its OWN exactly-sized allocation rather than
|
|
being carved out of `value` in place: that way each string is
|
|
followed by its own ASAN redzone, so a helper reading one byte
|
|
past the end of the username is reported precisely instead of
|
|
quietly running into the realm that would follow it in a shared
|
|
buffer. */
|
|
size_t o1 = vlen / 3;
|
|
size_t o2 = (2 * vlen) / 3;
|
|
char *user = fuzz_dup_n (value, o1);
|
|
char *realm = fuzz_dup_n (value + o1, o2 - o1);
|
|
char *pass = fuzz_dup_n (value + o2, vlen - o2);
|
|
|
|
if ( (NULL != user) && (NULL != realm) && (NULL != pass) )
|
|
{
|
|
if ( (0 != hs) &&
|
|
(hs <= 64) )
|
|
{
|
|
size_t claim = hs - (size_t) (data[0] % (unsigned int) (hs + 1u));
|
|
void *bin = malloc (claim);
|
|
|
|
if (NULL != bin)
|
|
{
|
|
(void) MHD_digest_auth_calc_userdigest (a, user, realm, pass,
|
|
bin, claim);
|
|
(void) MHD_digest_auth_calc_userhash (a, user, realm, bin, claim);
|
|
free (bin);
|
|
}
|
|
}
|
|
{
|
|
size_t need = (0 != hs) ? (2 * hs + 1) : 1;
|
|
size_t claim = need - (size_t) (data[0] % (unsigned int) (need + 1u));
|
|
char *hex = (char *) malloc (claim);
|
|
|
|
if (NULL != hex)
|
|
{
|
|
(void) MHD_digest_auth_calc_userhash_hex (a, user, realm,
|
|
hex, claim);
|
|
free (hex);
|
|
}
|
|
}
|
|
}
|
|
free (user);
|
|
free (realm);
|
|
free (pass);
|
|
}
|
|
#endif /* DAUTH_SUPPORT */
|
|
|
|
MHD_pool_destroy (pool);
|
|
free (value);
|
|
return 0;
|
|
}
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Generator */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static const char *const gen_param_names[] = {
|
|
"username", "username*", "realm", "nonce", "uri", "response", "algorithm",
|
|
"qop", "nc", "cnonce", "opaque", "userhash", "charset", "domain",
|
|
"unknown", "", "USERNAME", "user name"
|
|
};
|
|
|
|
static const char *const gen_param_values[] = {
|
|
"\"user\"", "user", "\"\"", "\"a\\\"b\"", "\"a\\\\\"", "\"\\\"",
|
|
"UTF-8''a%20b", "utf-8''%41", "''", "'", "true", "false", "TRUE",
|
|
"auth", "auth-int", "auth,auth-int", "\"auth\"", "MD5", "SHA-256",
|
|
"SHA-512-256", "BOGUS", "\"SHA-256\"", "00000001", "ffffffff",
|
|
"0123456789abcdef0123456789abcdef",
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
|
"\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\""
|
|
};
|
|
|
|
|
|
static size_t
|
|
fuzz_generate (struct fuzz_rng *rng,
|
|
uint8_t *buf,
|
|
size_t cap)
|
|
{
|
|
size_t len = 0;
|
|
unsigned int nparams;
|
|
unsigned int i;
|
|
int basic;
|
|
|
|
if (cap < 32)
|
|
return 0;
|
|
basic = fuzz_chance (rng, 4);
|
|
buf[len++] = (uint8_t) ((fuzz_byte (rng) & 0xFE) | (basic ? 1u : 0u));
|
|
|
|
/* NB: evaluate the argument exactly once -- it usually contains a
|
|
call into the PRNG. */
|
|
#define ADD(s) \
|
|
do { \
|
|
const char *s_ = (s); \
|
|
size_t l_ = strlen (s_); \
|
|
if (len + l_ >= cap) \
|
|
return len; \
|
|
memcpy (buf + len, s_, l_); \
|
|
len += l_; \
|
|
} while (0)
|
|
|
|
if (basic)
|
|
{
|
|
unsigned int n;
|
|
|
|
ADD ("Basic ");
|
|
n = fuzz_below (rng, 48);
|
|
for (i = 0; (i < n) && (len < cap); i++)
|
|
buf[len++] =
|
|
(uint8_t) "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
"0123456789+/= "[fuzz_below (rng, 66)];
|
|
return len;
|
|
}
|
|
|
|
ADD ("Digest ");
|
|
nparams = 1 + fuzz_below (rng, 12);
|
|
for (i = 0; i < nparams; i++)
|
|
{
|
|
if (0 != i)
|
|
ADD (fuzz_chance (rng, 8) ? "," : ", ");
|
|
ADD (gen_param_names[fuzz_below (rng,
|
|
(uint32_t) (sizeof (gen_param_names)
|
|
/ sizeof (char *)))]);
|
|
if (! fuzz_chance (rng, 10))
|
|
ADD (fuzz_chance (rng, 10) ? " = " : "=");
|
|
ADD (gen_param_values[fuzz_below (rng,
|
|
(uint32_t) (sizeof (gen_param_values)
|
|
/ sizeof (char *)))]);
|
|
}
|
|
#undef ADD
|
|
return len;
|
|
}
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Seed corpus */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
struct ah_seed
|
|
{
|
|
const char *txt;
|
|
size_t len;
|
|
};
|
|
|
|
#define ASEED(t) { t, sizeof (t) - 1 }
|
|
|
|
static const struct ah_seed ah_seeds[] = {
|
|
ASEED ("\x00" "Digest username=\"user\", realm=\"TestRealm\", "
|
|
"nonce=\"0123456789abcdef\", uri=\"/a\", qop=auth, nc=00000001, "
|
|
"cnonce=\"x\", algorithm=MD5, response=\"0123456789abcdef\""),
|
|
ASEED ("\x00" "Digest algorithm=BOGUS"),
|
|
ASEED ("\x00" "Digest algorithm="),
|
|
ASEED ("\x00" "Digest username*=UTF-8''a%20b"),
|
|
ASEED ("\x00" "Digest username*=''"),
|
|
ASEED ("\x00" "Digest username=\"\\\""),
|
|
ASEED ("\x00" "Digest userhash=true, username=\"aaaa\""),
|
|
ASEED ("\x00" "Digest response=\""
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\""),
|
|
ASEED ("\x00" "Digest"),
|
|
ASEED ("\x00" "Digest ,,,,,"),
|
|
ASEED ("\x00" "Digest nc=\"ffffffffffffffffffffffff\""),
|
|
ASEED ("\x01" "Basic dXNlcjpwYXNz"),
|
|
ASEED ("\x01" "Basic "),
|
|
ASEED ("\x01" "Basic ===="),
|
|
ASEED ("\x01" "Basic QQ==QQ=="),
|
|
|
|
/* Bit 0x02 of byte 0 additionally runs the connection-less digest
|
|
helpers (MHD_digest_auth_calc_userdigest/_userhash/_userhash_hex)
|
|
with the header value split into username / realm / password. Bits
|
|
5-7 pick the algorithm and byte 0 also sets the deliberately
|
|
undersized output-buffer length, so the seeds below cover several
|
|
algorithms at several buffer sizes. Without a seed here the branch
|
|
is only reachable by a lucky bit flip in byte 0.
|
|
|
|
The three *_SESSION algorithms are not seeded separately: they hash
|
|
with the same primitive as their non-session counterpart, so they
|
|
reach no code the entries below do not, and byte 0 is the byte a
|
|
mutator flips first anyway. */
|
|
ASEED ("\x02" "user:TestRealm:pass"),
|
|
ASEED ("\x22" "user:TestRealm:pass"),
|
|
ASEED ("\x42" "user:TestRealm:pass"),
|
|
ASEED ("\xa2" "user:TestRealm:pass"),
|
|
/* Degenerate splits: empty username, empty realm, empty password. */
|
|
ASEED ("\x02" "::"),
|
|
ASEED ("\x02" ""),
|
|
/* Both the parser and the helpers in one execution. */
|
|
ASEED ("\x02" "Digest username=\"user\", realm=\"TestRealm\"")
|
|
};
|
|
|
|
|
|
static size_t
|
|
fuzz_seed_count (void)
|
|
{
|
|
return sizeof (ah_seeds) / sizeof (ah_seeds[0]);
|
|
}
|
|
|
|
|
|
static const uint8_t *
|
|
fuzz_seed_get (size_t idx,
|
|
size_t *len)
|
|
{
|
|
*len = ah_seeds[idx].len;
|
|
return (const uint8_t *) ah_seeds[idx].txt;
|
|
}
|