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