diff --git a/fuzzing/ttyd/build_fuzzers.sh b/fuzzing/ttyd/build_fuzzers.sh
@@ -582,9 +582,9 @@ EOF
pty_buf_t *pty_buf_init(char *base, size_t len) { return NULL; }
void pty_buf_free(pty_buf_t *buf) {}
pty_process *process_init(void *ctx, uv_loop_t *loop,
- char *argv[], char *envp[]) { return NULL; }
+ char *argv[], char *envp[]) { return calloc(1, sizeof(pty_process)); }
bool process_running(pty_process *process) { return false; }
-void process_free(pty_process *process) {}
+void process_free(pty_process *process) { free(process); }
int pty_spawn(pty_process *process,
pty_read_cb read_cb,
pty_exit_cb exit_cb) { return -1; }
@@ -976,7 +976,7 @@ $CC $COMMON_CFLAGS fuzz_websocket_auth.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS -o
echo "[+] Built: fuzz_websocket_auth$SUFFIX"
echo "[*] Building fuzz_http_parsing..."
-$CC $COMMON_CFLAGS fuzz_http_parsing.c $MOCK_OBJS $JSON_C_LIBS -o fuzz_http_parsing$SUFFIX
+$CC $COMMON_CFLAGS fuzz_http_parsing.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS -o fuzz_http_parsing$SUFFIX
echo "[+] Built: fuzz_http_parsing$SUFFIX"
# CmpLog variants (AFL++ only)
@@ -988,7 +988,7 @@ if [ "$MODE" = "afl" ]; then
-o ${target}_cmplog
echo "[+] Built: ${target}_cmplog"
done
- AFL_LLVM_CMPLOG=1 $CC $COMMON_CFLAGS fuzz_http_parsing.c $MOCK_OBJS $JSON_C_LIBS \
+ AFL_LLVM_CMPLOG=1 $CC $COMMON_CFLAGS fuzz_http_parsing.c $MOCK_OBJS $JSON_C_LIBS $EXTRA_LIBS \
-o fuzz_http_parsing_cmplog
echo "[+] Built: fuzz_http_parsing_cmplog"
fi
diff --git a/fuzzing/ttyd/fuzz_http_parsing.c b/fuzzing/ttyd/fuzz_http_parsing.c
@@ -1,18 +1,27 @@
/*
- * AFL++ / LibFuzzer Harness for TTYD HTTP Request Parsing
+ * AFL++ / LibFuzzer harness — TTYD HTTP callback (full request handling)
*
- * Target: HTTP header parsing, path validation, credential extraction
- * Goal: Find parsing bugs, buffer overflows, injection vulnerabilities
+ * Target: the real callback_http() from ttyd's http.c
+ * Technique: #include the source file directly so static functions are
+ * accessible (standard OSS-Fuzz approach).
*
- * Compile with AFL++:
- * afl-clang-fast -fsanitize=address -g -O1 fuzz_http_parsing.c -o fuzz_http_parsing
+ * Exercises: check_auth(), access_log(), path routing (/token, /index,
+ * parent redirect, 404), accept_gzip(), uncompress_html(),
+ * LWS_CALLBACK_HTTP_WRITEABLE response path.
*
- * Compile with libFuzzer:
- * clang -fsanitize=fuzzer,address -g -O1 fuzz_http_parsing.c -o fuzz_http_parsing
+ * Input format (structured binary — pairs with http_mutator.so):
+ * [auth_mode:1][cred_len:2 BE][credential][path_len:2 BE][path][auth_header_value]
*
- * Run:
- * afl-fuzz -i input/ -o output/ ./fuzz_http_parsing
- * ./fuzz_http_parsing # libFuzzer
+ * auth_mode 0 — no auth required
+ * auth_mode 1 — Basic auth (credential = expected base64 value)
+ * auth_mode 2 — custom header auth (X-Auth-User)
+ *
+ * Build (handled by build_fuzzers.sh):
+ * afl-clang-fast -fsanitize=address,undefined -g -O1
+ * -I fuzz-include -I ttyd-src/src
+ * fuzz_http_parsing.c
+ * ttyd-obj/{utils,fuzz_lws_mock,mock_server_globals,mock_pty_stubs}.o
+ * -ljson-c -lz -o fuzz_http_parsing
*/
#include <stdio.h>
@@ -20,230 +29,125 @@
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
-#include <ctype.h>
-#include <unistd.h>
-
-#define MAX_PATH_LEN 128
-#define MAX_HEADER_LEN 256
-
-// Simulated HTTP parsing
-typedef struct {
- char path[MAX_PATH_LEN];
- char auth_header[MAX_HEADER_LEN];
- char origin_header[MAX_HEADER_LEN];
- char host_header[MAX_HEADER_LEN];
-} http_request_t;
-
-// Parse HTTP request from fuzzer input
-bool parse_http_request(const uint8_t *data, size_t size, http_request_t *req) {
- if (size == 0 || data == NULL || req == NULL) return false;
-
- memset(req, 0, sizeof(http_request_t));
-
- // Simple HTTP request parsing
- // Format: METHOD PATH\r\nHeader: Value\r\n\r\n
-
- const char *ptr = (const char *)data;
- const char *end = ptr + size;
-
- // Parse request line
- const char *path_start = strchr(ptr, ' ');
- if (!path_start || path_start >= end) return false;
- path_start++;
-
- const char *path_end = strchr(path_start, ' ');
- if (!path_end || path_end >= end) path_end = strchr(path_start, '\r');
- if (!path_end || path_end >= end) path_end = end;
-
- size_t path_len = path_end - path_start;
- if (path_len >= MAX_PATH_LEN) path_len = MAX_PATH_LEN - 1;
- memcpy(req->path, path_start, path_len);
- req->path[path_len] = '\0';
-
- // Parse headers
- ptr = path_end;
- while (ptr < end) {
- // Find header line
- const char *line_start = strchr(ptr, '\n');
- if (!line_start || line_start >= end) break;
- line_start++;
-
- if (line_start[0] == '\r' || line_start[0] == '\n') break; // End of headers
-
- const char *colon = strchr(line_start, ':');
- if (!colon || colon >= end) {
- ptr = line_start;
- continue;
- }
-
- // Extract header name and value
- size_t name_len = colon - line_start;
- const char *value_start = colon + 1;
- while (value_start < end && isspace(*value_start)) value_start++;
-
- const char *value_end = strchr(value_start, '\r');
- if (!value_end || value_end >= end) value_end = end;
-
- size_t value_len = value_end - value_start;
-
- // Check for specific headers
- if (name_len == 13 && strncasecmp(line_start, "Authorization", 13) == 0) {
- if (value_len >= MAX_HEADER_LEN) value_len = MAX_HEADER_LEN - 1;
- memcpy(req->auth_header, value_start, value_len);
- req->auth_header[value_len] = '\0';
- } else if (name_len == 6 && strncasecmp(line_start, "Origin", 6) == 0) {
- if (value_len >= MAX_HEADER_LEN) value_len = MAX_HEADER_LEN - 1;
- memcpy(req->origin_header, value_start, value_len);
- req->origin_header[value_len] = '\0';
- } else if (name_len == 4 && strncasecmp(line_start, "Host", 4) == 0) {
- if (value_len >= MAX_HEADER_LEN) value_len = MAX_HEADER_LEN - 1;
- memcpy(req->host_header, value_start, value_len);
- req->host_header[value_len] = '\0';
- }
-
- ptr = value_end;
- }
-
- return true;
-}
-
-// Validate path (check for path traversal, etc.)
-bool validate_path(const char *path) {
- if (path == NULL || path[0] == '\0') return false;
-
- // Check for path traversal sequences
- if (strstr(path, "..") != NULL) return false;
- if (strstr(path, "//") != NULL) return false;
-
- // Check for null bytes
- size_t len = strlen(path);
- for (size_t i = 0; i < len; i++) {
- if (path[i] == '\0') return false;
- }
-
- return true;
-}
-
-// Check origin vs host (CSRF protection)
-bool check_origin(const char *origin, const char *host) {
- if (origin == NULL || host == NULL) return false;
- if (origin[0] == '\0' || host[0] == '\0') return false;
-
- // Extract hostname from origin (http://host:port)
- const char *host_start = strstr(origin, "://");
- if (host_start) host_start += 3;
- else host_start = origin;
-
- // Compare with Host header
- return strncasecmp(host_start, host, strlen(host)) == 0;
-}
-// Extract and validate Basic auth
-bool extract_basic_auth(const char *auth_header, char *username, char *password, size_t max_len) {
- if (auth_header == NULL || username == NULL || password == NULL) return false;
+/* Mock lws control state — must come before including the real ttyd source */
+#include "fuzz_lws_mock.h"
- // Check for "Basic " prefix
- if (strncmp(auth_header, "Basic ", 6) != 0) return false;
+/* Pull in the real ttyd HTTP handler (callback_http, check_auth, etc.) */
+#include "ttyd-src/src/http.c"
- const char *encoded = auth_header + 6;
-
- // Simple base64 decode (not production quality)
- // For fuzzing purposes, just extract the string
- size_t len = strlen(encoded);
- if (len >= max_len) len = max_len - 1;
-
- // Look for colon separator in decoded value (simplified)
- const char *colon = strchr(encoded, ':');
- if (colon) {
- size_t user_len = colon - encoded;
- if (user_len >= max_len) user_len = max_len - 1;
- memcpy(username, encoded, user_len);
- username[user_len] = '\0';
-
- size_t pass_len = len - user_len - 1;
- if (pass_len >= max_len) pass_len = max_len - 1;
- memcpy(password, colon + 1, pass_len);
- password[pass_len] = '\0';
- }
+/* Provided by mock_server_globals.c */
+extern struct server _fuzz_server;
+extern void fuzz_server_reset(void);
- return true;
-}
+/* ── fuzz target ─────────────────────────────────────────────────────────── */
-// Fuzzing target
int fuzz_target(const uint8_t *data, size_t size) {
- if (size < 10) return 0;
-
- http_request_t req;
- if (!parse_http_request(data, size, &req)) {
- return 0;
+ /* Need at least: auth_mode(1) + cred_len(2) + path_len(2) = 5 bytes */
+ if (size < 5) return 0;
+
+ /* Parse structured input */
+ uint8_t auth_mode = data[0] % 3;
+ uint16_t cred_len = (uint16_t)((data[1] << 8) | data[2]);
+ if ((size_t)cred_len > size - 5) cred_len = (uint16_t)(size - 5);
+
+ size_t off = 3 + cred_len;
+ if (off + 2 > size) return 0;
+
+ uint16_t path_len = (uint16_t)((data[off] << 8) | data[off + 1]);
+ off += 2;
+ if ((size_t)path_len > size - off) path_len = (uint16_t)(size - off);
+
+ /* Reset all fuzzer-controlled state */
+ fuzz_lws_reset();
+ fuzz_server_reset();
+
+ /* Set up the credential the server expects */
+ static char cred_buf[65536];
+ if (auth_mode == 1 && cred_len > 0) {
+ memcpy(cred_buf, data + 3, cred_len);
+ cred_buf[cred_len] = '\0';
+ _fuzz_server.credential = cred_buf;
+ _fuzz_server.auth_header = NULL;
+ } else if (auth_mode == 2) {
+ _fuzz_server.credential = NULL;
+ _fuzz_server.auth_header = (char *)"X-Auth-User";
+ }
+ /* auth_mode 0: no auth, both remain NULL */
+
+ /* Extract the URL path (null-terminated for callback_http) */
+ static char path_buf[4096];
+ int n = path_len < (int)sizeof(path_buf) - 1 ? path_len : (int)sizeof(path_buf) - 1;
+ memcpy(path_buf, data + off, n);
+ path_buf[n] = '\0';
+ off += path_len;
+
+ /* Remaining bytes are the Authorization header value */
+ size_t auth_len = (off < size) ? size - off : 0;
+ fuzz_lws_set_auth((const char *)(data + off), (int)auth_len);
+
+ /* For custom-header mode, same bytes go to the custom header mock */
+ if (auth_mode == 2)
+ fuzz_lws_set_custom((const char *)(data + off), (int)auth_len);
+
+ /* ── Exercise the REAL callback_http() ────────────────────────────── */
+ struct lws mock_wsi = {0};
+ struct pss_http pss = {0};
+
+ /* LWS_CALLBACK_HTTP: main request handling (auth, path routing, etc.) */
+ int ret = callback_http(&mock_wsi, LWS_CALLBACK_HTTP,
+ &pss, path_buf, (size_t)n);
+
+ /* LWS_CALLBACK_HTTP_WRITEABLE: exercise response body transmission
+ * (only meaningful if the HTTP handler set up a buffer) */
+ if (ret == 0 && pss.buffer != NULL && pss.len > 0) {
+ (void)callback_http(&mock_wsi, LWS_CALLBACK_HTTP_WRITEABLE,
+ &pss, NULL, 0);
+ /* pss_buffer_free() inside WRITEABLE already freed the buffer —
+ * null it so the cleanup below doesn't double-free. */
+ pss.buffer = NULL;
}
- // Validate parsed data
- bool path_valid = validate_path(req.path);
- bool origin_valid = check_origin(req.origin_header, req.host_header);
-
- char username[128] = {0};
- char password[128] = {0};
- bool auth_valid = extract_basic_auth(req.auth_header, username, password, sizeof(username));
-
- // Exercise various code paths
- if (path_valid && origin_valid && auth_valid) {
- // All validations passed
+ /* Clean up any strdup'd buffer from /token path if WRITEABLE was skipped */
+ if (pss.buffer != NULL && pss.buffer != (char *)index_html &&
+ pss.buffer != html_cache) {
+ free(pss.buffer);
}
return 0;
}
+/* ── AFL++ persistent mode ───────────────────────────────────────────────── */
#ifdef __AFL_FUZZ_TESTCASE_LEN
-// AFL++ persistent mode
__AFL_FUZZ_INIT();
-
int main(void) {
- #ifdef __AFL_HAVE_MANUAL_CONTROL
- __AFL_INIT();
- #endif
-
+#ifdef __AFL_HAVE_MANUAL_CONTROL
+ __AFL_INIT();
+#endif
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
-
while (__AFL_LOOP(10000)) {
- int len = __AFL_FUZZ_TESTCASE_LEN;
- fuzz_target(buf, len);
+ fuzz_target(buf, __AFL_FUZZ_TESTCASE_LEN);
}
-
return 0;
}
+
#else
-// LibFuzzer mode
+/* LibFuzzer */
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
return fuzz_target(data, size);
}
-// Standalone mode for testing
#ifndef LIBFUZZER
+/* Standalone file-based testing */
int main(int argc, char **argv) {
- if (argc != 2) {
- fprintf(stderr, "Usage: %s <input_file>\n", argv[0]);
- return 1;
- }
-
+ if (argc != 2) { fprintf(stderr, "Usage: %s <input_file>\n", argv[0]); return 1; }
FILE *f = fopen(argv[1], "rb");
- if (!f) {
- perror("fopen");
- return 1;
- }
-
- fseek(f, 0, SEEK_END);
- long size = ftell(f);
- fseek(f, 0, SEEK_SET);
-
- uint8_t *data = malloc(size);
- fread(data, 1, size, f);
- fclose(f);
-
- fuzz_target(data, size);
- free(data);
-
- printf("Test completed successfully\n");
+ if (!f) { perror("fopen"); return 1; }
+ fseek(f, 0, SEEK_END); long sz = ftell(f); rewind(f);
+ uint8_t *buf = malloc(sz);
+ fread(buf, 1, sz, f); fclose(f);
+ fuzz_target(buf, sz);
+ free(buf);
return 0;
}
#endif
| |