etc.git


etc

~ scripts and random things
git clone https://github.com/djnnvx/etc.git
log | files | refs | archive

fuzz_websocket_auth.c (8236B)

      1 /*
      2  * AFL++ / LibFuzzer harness — TTYD WebSocket protocol (callback_tty)
      3  *
      4  * Exercises the full websocket lifecycle: connection filtering, auth,
      5  * origin checking, message receive (INPUT, RESIZE, JSON_DATA, etc.),
      6  * writable callbacks, and connection close/cleanup.
      7  *
      8  * Input format:
      9  *   [flags:1][cred_len:2 BE][credential]
     10  *   [path_len:1][path][origin_len:1][origin][host_len:1][host]
     11  *   [ws_message...]
     12  *
     13  * Flags byte:
     14  *   bit 0: has credential (Basic auth)
     15  *   bit 1: has auth_header (custom header proxy auth)
     16  *   bit 2: check_origin enabled
     17  *   bit 3: url_arg enabled
     18  *   bit 4: writable
     19  */
     20 
     21 #include <stdio.h>
     22 #include <stdlib.h>
     23 #include <string.h>
     24 #include <stdint.h>
     25 #include <stdbool.h>
     26 #include <json-c/json.h>
     27 
     28 /* Mock lws control state — must come before the real ttyd source */
     29 #include "fuzz_lws_mock.h"
     30 
     31 /* Pull in real ttyd protocol code (callback_tty and all static helpers) */
     32 #include "ttyd-src/src/protocol.c"
     33 
     34 /* Provided by mock_server_globals.c */
     35 extern struct server  _fuzz_server;
     36 extern void           fuzz_server_reset(void);
     37 
     38 /* ── input parser ──────────────────────────────────────────────────── */
     39 
     40 /* Read a 1-byte length-prefixed field from data. Returns bytes consumed. */
     41 static size_t read_field_1(const uint8_t *data, size_t avail,
     42                            char *out, int *out_len, int max) {
     43     if (avail < 1) { *out_len = 0; return 0; }
     44     int flen = data[0];
     45     if (flen > (int)(avail - 1)) flen = (int)(avail - 1);
     46     if (flen > max) flen = max;
     47     memcpy(out, data + 1, flen);
     48     out[flen] = '\0';
     49     *out_len = flen;
     50     return 1 + flen;
     51 }
     52 
     53 /* ── fuzz target ───────────────────────────────────────────────────── */
     54 
     55 int fuzz_target(const uint8_t *data, size_t size) {
     56     /* Minimum: flags(1) + cred_len(2) + path_len(1) + origin_len(1) + host_len(1) */
     57     if (size < 6) return 0;
     58 
     59     /* Parse flags */
     60     uint8_t flags = data[0];
     61     bool has_credential  = (flags & 0x01) != 0;
     62     bool has_auth_header = (flags & 0x02) != 0;
     63     bool check_origin    = (flags & 0x04) != 0;
     64     bool url_arg         = (flags & 0x08) != 0;
     65     bool writable        = (flags & 0x10) != 0;
     66 
     67     /* Parse credential */
     68     uint16_t cred_len = (uint16_t)((data[1] << 8) | data[2]);
     69     size_t off = 3;
     70     if (cred_len > size - off) cred_len = (uint16_t)(size - off);
     71 
     72     static char cred_buf[65536];
     73     memcpy(cred_buf, data + off, cred_len);
     74     cred_buf[cred_len] = '\0';
     75     off += cred_len;
     76 
     77     /* Parse length-prefixed fields: path, origin, host */
     78     char path_buf[4096], origin_buf[4096], host_buf[4096];
     79     int path_len = 0, origin_len = 0, host_len = 0;
     80     size_t n;
     81 
     82     n = read_field_1(data + off, size - off, path_buf, &path_len, 4095);
     83     off += n;
     84     n = read_field_1(data + off, size - off, origin_buf, &origin_len, 4095);
     85     off += n;
     86     n = read_field_1(data + off, size - off, host_buf, &host_len, 4095);
     87     off += n;
     88 
     89     /* Remaining bytes = websocket message payload */
     90     const uint8_t *ws_msg = (off < size) ? data + off : NULL;
     91     size_t ws_len = (off < size) ? size - off : 0;
     92 
     93     /* ── Reset mock state ─────────────────────────────────────────── */
     94     fuzz_lws_reset();
     95     fuzz_server_reset();
     96 
     97     /* Configure server */
     98     _fuzz_server.writable     = writable;
     99     _fuzz_server.check_origin = check_origin;
    100     _fuzz_server.url_arg      = url_arg;
    101     _fuzz_server.command      = "bash";
    102     _fuzz_server.prefs_json   = "{}";
    103     _fuzz_server.terminal_type[0] = '\0';
    104     strncpy(_fuzz_server.terminal_type, "xterm-256color",
    105             sizeof(_fuzz_server.terminal_type) - 1);
    106 
    107     if (has_credential && cred_len > 0) {
    108         _fuzz_server.credential = cred_buf;
    109     }
    110     if (has_auth_header) {
    111         _fuzz_server.auth_header = "X-Auth-User";
    112         /* Put credential in custom header mock too */
    113         fuzz_lws_set_custom(cred_buf, cred_len);
    114     }
    115 
    116     /* Set up fuzzer-controlled headers */
    117     if (has_credential) {
    118         fuzz_lws_set_auth(cred_buf, cred_len);
    119     }
    120     fuzz_lws_set_uri_path(path_buf, path_len);
    121     fuzz_lws_set_origin(origin_buf, origin_len);
    122     fuzz_lws_set_host(host_buf, host_len);
    123 
    124     /* Set up URI args from the ws_msg if url_arg is enabled.
    125      * We'll stuff "arg=<first 50 bytes of msg>" as a URI arg fragment
    126      * to exercise the arg-parsing path in ESTABLISHED. */
    127     if (url_arg && ws_len > 0) {
    128         int arg_len = ws_len > 50 ? 50 : (int)ws_len;
    129         char arg_buf[256];
    130         memcpy(arg_buf, "arg=", 4);
    131         memcpy(arg_buf + 4, ws_msg, arg_len);
    132         arg_buf[4 + arg_len] = '\0';
    133         memcpy(_fuzz_uri_args[0], arg_buf, 4 + arg_len + 1);
    134         _fuzz_uri_args_len[0] = 4 + arg_len;
    135         _fuzz_uri_args_count = 1;
    136     }
    137 
    138     /* ── Simulate WebSocket lifecycle ─────────────────────────────── */
    139     struct lws      mock_wsi = {0};
    140     struct pss_tty  pss;
    141     memset(&pss, 0, sizeof(pss));
    142 
    143     /* Step 1: FILTER_PROTOCOL_CONNECTION (auth + path + origin check) */
    144     int ret = callback_tty(&mock_wsi, LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION,
    145                            &pss, NULL, 0);
    146     if (ret != 0) goto cleanup;
    147 
    148     /* Step 2: ESTABLISHED (client setup, URL arg parsing) */
    149     ret = callback_tty(&mock_wsi, LWS_CALLBACK_ESTABLISHED,
    150                        &pss, NULL, 0);
    151     if (ret != 0) goto cleanup;
    152 
    153     /* Step 3: SERVER_WRITEABLE (send initial messages) */
    154     callback_tty(&mock_wsi, LWS_CALLBACK_SERVER_WRITEABLE, &pss, NULL, 0);
    155 
    156     /* Step 4: RECEIVE with the fuzz payload */
    157     if (ws_msg && ws_len > 0) {
    158         /* Make a mutable copy — callback_tty may modify the buffer via
    159          * xrealloc on pss->buffer, but the initial 'in' pointer is just read */
    160         char *msg_copy = malloc(ws_len);
    161         if (msg_copy) {
    162             memcpy(msg_copy, ws_msg, ws_len);
    163             ret = callback_tty(&mock_wsi, LWS_CALLBACK_RECEIVE,
    164                                &pss, msg_copy, ws_len);
    165             free(msg_copy);
    166 
    167             /* If RECEIVE succeeded and the command was JSON_DATA,
    168              * try another WRITEABLE to exercise the post-auth path */
    169             if (ret == 0) {
    170                 callback_tty(&mock_wsi, LWS_CALLBACK_SERVER_WRITEABLE,
    171                              &pss, NULL, 0);
    172             }
    173         }
    174     }
    175 
    176 cleanup:
    177     /* Step 5: CLOSED (cleanup buffers, args, etc.)
    178      * Do NOT override pss.wsi here.  ttyd only sets pss->wsi after a
    179      * successful pty_spawn + LIST_INSERT_HEAD.  If FILTER rejected the
    180      * connection, or ESTABLISHED failed (spawn always returns -1 in the
    181      * mock), pss->wsi is NULL and ttyd's CLOSED handler skips LIST_REMOVE —
    182      * which is correct, since pss was never inserted.  Forcing pss->wsi
    183      * non-NULL here would make LIST_REMOVE deref a NULL le_prev and crash,
    184      * producing harness artifacts rather than real bugs. */
    185     callback_tty(&mock_wsi, LWS_CALLBACK_CLOSED, &pss, NULL, 0);
    186 
    187     return 0;
    188 }
    189 
    190 /* ── AFL++ persistent mode ───────────────────────────────────────────── */
    191 #ifdef __AFL_FUZZ_TESTCASE_LEN
    192 __AFL_FUZZ_INIT();
    193 int main(void) {
    194 #ifdef __AFL_HAVE_MANUAL_CONTROL
    195     __AFL_INIT();
    196 #endif
    197     unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
    198     while (__AFL_LOOP(10000)) {
    199         fuzz_target(buf, __AFL_FUZZ_TESTCASE_LEN);
    200     }
    201     return 0;
    202 }
    203 
    204 #else
    205 int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    206     return fuzz_target(data, size);
    207 }
    208 
    209 #ifndef LIBFUZZER
    210 int main(int argc, char **argv) {
    211     if (argc != 2) { fprintf(stderr, "Usage: %s <input_file>\n", argv[0]); return 1; }
    212     FILE *f = fopen(argv[1], "rb");
    213     if (!f) { perror("fopen"); return 1; }
    214     fseek(f, 0, SEEK_END); long sz = ftell(f); rewind(f);
    215     uint8_t *buf = malloc(sz);
    216     fread(buf, 1, sz, f); fclose(f);
    217     fuzz_target(buf, sz);
    218     free(buf);
    219     return 0;
    220 }
    221 #endif
    222 #endif