diff --git a/fuzzing/ttyd/fuzz_lws_mock.c b/fuzzing/ttyd/fuzz_lws_mock.c
@@ -7,62 +7,135 @@
#include <string.h>
#include <stdio.h>
#include <stdint.h>
+#include <stdlib.h>
/* ── fuzzer-controlled state ─────────────────────────────────────────── */
char _fuzz_auth_header[4096];
int _fuzz_auth_header_len;
char _fuzz_custom_header[4096];
int _fuzz_custom_header_len;
+char _fuzz_uri_path[4096];
+int _fuzz_uri_path_len;
+char _fuzz_origin[4096];
+int _fuzz_origin_len;
+char _fuzz_host[4096];
+int _fuzz_host_len;
+
+char _fuzz_uri_args[FUZZ_MAX_URI_ARGS][256];
+int _fuzz_uri_args_len[FUZZ_MAX_URI_ARGS];
+int _fuzz_uri_args_count;
void fuzz_lws_reset(void) {
memset(_fuzz_auth_header, 0, sizeof(_fuzz_auth_header));
_fuzz_auth_header_len = 0;
memset(_fuzz_custom_header, 0, sizeof(_fuzz_custom_header));
_fuzz_custom_header_len = 0;
+ memset(_fuzz_uri_path, 0, sizeof(_fuzz_uri_path));
+ _fuzz_uri_path_len = 0;
+ memset(_fuzz_origin, 0, sizeof(_fuzz_origin));
+ _fuzz_origin_len = 0;
+ memset(_fuzz_host, 0, sizeof(_fuzz_host));
+ _fuzz_host_len = 0;
+ memset(_fuzz_uri_args, 0, sizeof(_fuzz_uri_args));
+ memset(_fuzz_uri_args_len, 0, sizeof(_fuzz_uri_args_len));
+ _fuzz_uri_args_count = 0;
}
/* ── header accessors ────────────────────────────────────────────────── */
-int lws_hdr_copy(struct lws *wsi, char *dest, int len,
- enum lws_token_indexes h) {
- const char *src = NULL;
- int srclen = 0;
- if (h == WSI_TOKEN_HTTP_AUTHORIZATION) {
- src = _fuzz_auth_header;
- srclen = _fuzz_auth_header_len;
- }
- if (!src || !dest || len <= 0) { if (dest && len > 0) dest[0] = '\0'; return 0; }
+
+/* Helper: copy from a source buffer into dest, return bytes copied */
+static int copy_hdr(char *dest, int len, const char *src, int srclen) {
+ if (!dest || len <= 0) return 0;
+ if (!src || srclen <= 0) { dest[0] = '\0'; return 0; }
int n = srclen < len - 1 ? srclen : len - 1;
memcpy(dest, src, n);
dest[n] = '\0';
return n;
}
+int lws_hdr_copy(struct lws *wsi, char *dest, int len,
+ enum lws_token_indexes h) {
+ switch (h) {
+ case WSI_TOKEN_HTTP_AUTHORIZATION:
+ return copy_hdr(dest, len, _fuzz_auth_header, _fuzz_auth_header_len);
+ case WSI_TOKEN_GET_URI:
+ case WSI_TOKEN_HTTP_COLON_PATH:
+ return copy_hdr(dest, len, _fuzz_uri_path, _fuzz_uri_path_len);
+ case WSI_TOKEN_ORIGIN:
+ return copy_hdr(dest, len, _fuzz_origin, _fuzz_origin_len);
+ case WSI_TOKEN_HOST:
+ return copy_hdr(dest, len, _fuzz_host, _fuzz_host_len);
+ default:
+ if (dest && len > 0) dest[0] = '\0';
+ return 0;
+ }
+}
+
int lws_hdr_custom_length(struct lws *wsi, const char *name, int nlen) {
return _fuzz_custom_header_len;
}
int lws_hdr_custom_copy(struct lws *wsi, char *dst, int len,
const char *name, int nlen) {
- if (!dst || len <= 0) return 0;
- int n = _fuzz_custom_header_len < len - 1 ? _fuzz_custom_header_len : len - 1;
- memcpy(dst, _fuzz_custom_header, n);
- dst[n] = '\0';
- return n;
+ return copy_hdr(dst, len, _fuzz_custom_header, _fuzz_custom_header_len);
}
int lws_hdr_copy_fragment(struct lws *wsi, char *dst, int len,
enum lws_token_indexes h, int frag_idx) {
+ if (h == WSI_TOKEN_HTTP_URI_ARGS &&
+ frag_idx < _fuzz_uri_args_count && frag_idx < FUZZ_MAX_URI_ARGS) {
+ return copy_hdr(dst, len,
+ _fuzz_uri_args[frag_idx],
+ _fuzz_uri_args_len[frag_idx]);
+ }
if (dst && len > 0) dst[0] = '\0';
return 0;
}
/* ── URI parsing ─────────────────────────────────────────────────────── */
+/* Simple parser that extracts protocol, host, port, path from a URI.
+ * Modifies the input string in-place (like the real lws_parse_uri). */
int lws_parse_uri(char *p, const char **prot, const char **host,
int *port, const char **path) {
- if (prot) *prot = "";
- if (host) *host = "";
- if (port) *port = 0;
- if (path) *path = p ? p : "";
+ if (!p || !*p) {
+ if (prot) *prot = "";
+ if (host) *host = "";
+ if (port) *port = 0;
+ if (path) *path = "";
+ return -1;
+ }
+
+ /* Find protocol: "http://" or "https://" etc. */
+ char *sep = strstr(p, "://");
+ if (sep) {
+ *sep = '\0';
+ if (prot) *prot = p;
+ p = sep + 3;
+ } else {
+ if (prot) *prot = "";
+ }
+
+ /* host[:port][/path] */
+ if (host) *host = p;
+
+ /* Find path separator */
+ char *slash = strchr(p, '/');
+ if (slash) {
+ if (path) *path = slash + 1;
+ *slash = '\0';
+ } else {
+ if (path) *path = "";
+ }
+
+ /* Find port in host part */
+ char *colon = strchr(p, ':');
+ if (colon) {
+ *colon = '\0';
+ if (port) *port = atoi(colon + 1);
+ } else {
+ if (port) *port = 0;
+ }
+
return 0;
}
diff --git a/fuzzing/ttyd/fuzz_websocket_auth.c b/fuzzing/ttyd/fuzz_websocket_auth.c
@@ -1,19 +1,21 @@
/*
- * AFL++ / LibFuzzer harness — TTYD WebSocket AuthToken + window-size JSON
+ * AFL++ / LibFuzzer harness — TTYD WebSocket protocol (callback_tty)
*
- * Target: the real parse_window_size() and check_auth() from ttyd's protocol.c
- * Technique: #include the source file so static functions are accessible.
+ * Exercises the full websocket lifecycle: connection filtering, auth,
+ * origin checking, message receive (INPUT, RESIZE, JSON_DATA, etc.),
+ * writable callbacks, and connection close/cleanup.
*
- * Input format: [has_credential:1][credential_len:2 BE][credential][json_message]
- * has_credential odd → server->credential is set to the credential bytes
- * has_credential even → no credential (open server)
+ * Input format:
+ * [flags:1][cred_len:2 BE][credential]
+ * [path_len:1][path][origin_len:1][origin][host_len:1][host]
+ * [ws_message...]
*
- * Build (handled by build_fuzzers.sh):
- * afl-clang-fast -fsanitize=address,undefined -g -O1
- * -I fuzz-include -I ttyd-src/src
- * fuzz_websocket_auth.c
- * ttyd-obj/{http,protocol,utils,fuzz_lws_mock,mock_server_globals,mock_pty_stubs}.o
- * -ljson-c -o fuzz_websocket_auth
+ * Flags byte:
+ * bit 0: has credential (Basic auth)
+ * bit 1: has auth_header (custom header proxy auth)
+ * bit 2: check_origin enabled
+ * bit 3: url_arg enabled
+ * bit 4: writable
*/
#include <stdio.h>
@@ -26,69 +28,160 @@
/* Mock lws control state — must come before the real ttyd source */
#include "fuzz_lws_mock.h"
-/* Pull in real ttyd protocol code (parse_window_size, check_auth static fns) */
+/* Pull in real ttyd protocol code (callback_tty and all static helpers) */
#include "ttyd-src/src/protocol.c"
/* Provided by mock_server_globals.c */
extern struct server _fuzz_server;
extern void fuzz_server_reset(void);
-/* ── fuzz target ─────────────────────────────────────────────────────────── */
+/* ── input parser ──────────────────────────────────────────────────── */
+
+/* Read a 1-byte length-prefixed field from data. Returns bytes consumed. */
+static size_t read_field_1(const uint8_t *data, size_t avail,
+ char *out, int *out_len, int max) {
+ if (avail < 1) { *out_len = 0; return 0; }
+ int flen = data[0];
+ if (flen > (int)(avail - 1)) flen = (int)(avail - 1);
+ if (flen > max) flen = max;
+ memcpy(out, data + 1, flen);
+ out[flen] = '\0';
+ *out_len = flen;
+ return 1 + flen;
+}
-int fuzz_target(const uint8_t *data, size_t size) {
- if (size < 3) return 0;
+/* ── fuzz target ───────────────────────────────────────────────────── */
- bool has_credential = (data[0] & 1) == 1;
- uint16_t cred_len = (uint16_t)((data[1] << 8) | data[2]);
- if ((size_t)cred_len > size - 3) cred_len = (uint16_t)(size - 3);
+int fuzz_target(const uint8_t *data, size_t size) {
+ /* Minimum: flags(1) + cred_len(2) + path_len(1) + origin_len(1) + host_len(1) */
+ if (size < 6) return 0;
+
+ /* Parse flags */
+ uint8_t flags = data[0];
+ bool has_credential = (flags & 0x01) != 0;
+ bool has_auth_header = (flags & 0x02) != 0;
+ bool check_origin = (flags & 0x04) != 0;
+ bool url_arg = (flags & 0x08) != 0;
+ bool writable = (flags & 0x10) != 0;
+
+ /* Parse credential */
+ uint16_t cred_len = (uint16_t)((data[1] << 8) | data[2]);
+ size_t off = 3;
+ if (cred_len > size - off) cred_len = (uint16_t)(size - off);
+ static char cred_buf[65536];
+ memcpy(cred_buf, data + off, cred_len);
+ cred_buf[cred_len] = '\0';
+ off += cred_len;
+
+ /* Parse length-prefixed fields: path, origin, host */
+ char path_buf[4096], origin_buf[4096], host_buf[4096];
+ int path_len = 0, origin_len = 0, host_len = 0;
+ size_t n;
+
+ n = read_field_1(data + off, size - off, path_buf, &path_len, 4095);
+ off += n;
+ n = read_field_1(data + off, size - off, origin_buf, &origin_len, 4095);
+ off += n;
+ n = read_field_1(data + off, size - off, host_buf, &host_len, 4095);
+ off += n;
+
+ /* Remaining bytes = websocket message payload */
+ const uint8_t *ws_msg = (off < size) ? data + off : NULL;
+ size_t ws_len = (off < size) ? size - off : 0;
+
+ /* ── Reset mock state ─────────────────────────────────────────── */
fuzz_lws_reset();
fuzz_server_reset();
- /* Configure server credential */
- static char cred_buf[65536];
+ /* Configure server */
+ _fuzz_server.writable = writable;
+ _fuzz_server.check_origin = check_origin;
+ _fuzz_server.url_arg = url_arg;
+ _fuzz_server.command = "bash";
+ _fuzz_server.prefs_json = "{}";
+ _fuzz_server.terminal_type[0] = '\0';
+ strncpy(_fuzz_server.terminal_type, "xterm-256color",
+ sizeof(_fuzz_server.terminal_type) - 1);
+
if (has_credential && cred_len > 0) {
- memcpy(cred_buf, data + 3, cred_len);
- cred_buf[cred_len] = '\0';
_fuzz_server.credential = cred_buf;
}
- /* lws auth header: supply the same credential bytes so the WS
- * check_auth() path (which uses lws_hdr_copy) can also be explored */
- fuzz_lws_set_auth((const char *)cred_buf, has_credential ? cred_len : 0);
-
- /* JSON message starts after the credential */
- size_t json_off = 3 + cred_len;
- if (json_off >= size) return 0;
-
- const char *json_data = (const char *)(data + json_off);
- size_t json_len = size - json_off;
-
- /* Exercise the real parse_window_size() from protocol.c */
- uint16_t cols = 0, rows = 0;
- struct json_object *obj = parse_window_size(json_data, (int)json_len,
- &cols, &rows);
- if (!obj) return 0;
-
- /* Exercise AuthToken validation when credential is configured */
- if (_fuzz_server.credential) {
- struct json_object *tok_obj = NULL;
- if (json_object_object_get_ex(obj, "AuthToken", &tok_obj)) {
- const char *token = json_object_get_string(tok_obj);
- if (token)
- (void)strcmp(token, _fuzz_server.credential);
- }
+ if (has_auth_header) {
+ _fuzz_server.auth_header = "X-Auth-User";
+ /* Put credential in custom header mock too */
+ fuzz_lws_set_custom(cred_buf, cred_len);
}
- /* Also exercise the WS check_auth() static function */
+ /* Set up fuzzer-controlled headers */
+ if (has_credential) {
+ fuzz_lws_set_auth(cred_buf, cred_len);
+ }
+ fuzz_lws_set_uri_path(path_buf, path_len);
+ fuzz_lws_set_origin(origin_buf, origin_len);
+ fuzz_lws_set_host(host_buf, host_len);
+
+ /* Set up URI args from the ws_msg if url_arg is enabled.
+ * We'll stuff "arg=<first 50 bytes of msg>" as a URI arg fragment
+ * to exercise the arg-parsing path in ESTABLISHED. */
+ if (url_arg && ws_len > 0) {
+ int arg_len = ws_len > 50 ? 50 : (int)ws_len;
+ char arg_buf[256];
+ memcpy(arg_buf, "arg=", 4);
+ memcpy(arg_buf + 4, ws_msg, arg_len);
+ arg_buf[4 + arg_len] = '\0';
+ memcpy(_fuzz_uri_args[0], arg_buf, 4 + arg_len + 1);
+ _fuzz_uri_args_len[0] = 4 + arg_len;
+ _fuzz_uri_args_count = 1;
+ }
+
+ /* ── Simulate WebSocket lifecycle ─────────────────────────────── */
struct lws mock_wsi = {0};
- struct pss_tty pss = {0};
- (void)check_auth(&mock_wsi, &pss);
+ struct pss_tty pss;
+ memset(&pss, 0, sizeof(pss));
+
+ /* Step 1: FILTER_PROTOCOL_CONNECTION (auth + path + origin check) */
+ int ret = callback_tty(&mock_wsi, LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION,
+ &pss, NULL, 0);
+ if (ret != 0) goto cleanup;
+
+ /* Step 2: ESTABLISHED (client setup, URL arg parsing) */
+ ret = callback_tty(&mock_wsi, LWS_CALLBACK_ESTABLISHED,
+ &pss, NULL, 0);
+ if (ret != 0) goto cleanup;
+
+ /* Step 3: SERVER_WRITEABLE (send initial messages) */
+ callback_tty(&mock_wsi, LWS_CALLBACK_SERVER_WRITEABLE, &pss, NULL, 0);
+
+ /* Step 4: RECEIVE with the fuzz payload */
+ if (ws_msg && ws_len > 0) {
+ /* Make a mutable copy — callback_tty may modify the buffer via
+ * xrealloc on pss->buffer, but the initial 'in' pointer is just read */
+ char *msg_copy = malloc(ws_len);
+ if (msg_copy) {
+ memcpy(msg_copy, ws_msg, ws_len);
+ ret = callback_tty(&mock_wsi, LWS_CALLBACK_RECEIVE,
+ &pss, msg_copy, ws_len);
+ free(msg_copy);
+
+ /* If RECEIVE succeeded and the command was JSON_DATA,
+ * try another WRITEABLE to exercise the post-auth path */
+ if (ret == 0) {
+ callback_tty(&mock_wsi, LWS_CALLBACK_SERVER_WRITEABLE,
+ &pss, NULL, 0);
+ }
+ }
+ }
+
+cleanup:
+ /* Step 5: CLOSED (cleanup buffers, args, etc.) */
+ pss.wsi = &mock_wsi; /* CLOSED checks pss->wsi != NULL */
+ callback_tty(&mock_wsi, LWS_CALLBACK_CLOSED, &pss, NULL, 0);
- json_object_put(obj);
return 0;
}
-/* ── AFL++ persistent mode ───────────────────────────────────────────────── */
+/* ── AFL++ persistent mode ───────────────────────────────────────────── */
#ifdef __AFL_FUZZ_TESTCASE_LEN
__AFL_FUZZ_INIT();
int main(void) {
diff --git a/fuzzing/ttyd/gen_corpus.py b/fuzzing/ttyd/gen_corpus.py
@@ -5,8 +5,12 @@ Creates structured binary seeds in corpus/{websocket_auth,http_parsing}/.
Run from the script directory or pass the output base dir as argv[1].
Formats:
- websocket_auth: [has_credential:1][cred_len:2 BE][credential][json_message]
- http_parsing: [auth_mode:1][cred_len:2 BE][credential][path_len:2 BE][path][auth_header_value]
+ websocket_auth: [flags:1][cred_len:2 BE][cred][path_len:1][path]
+ [origin_len:1][origin][host_len:1][host][ws_message]
+ flags: bit0=has_cred, bit1=auth_header, bit2=check_origin,
+ bit3=url_arg, bit4=writable
+
+ http_parsing: [auth_mode:1][cred_len:2 BE][cred][path_len:2 BE][path][auth_header_value]
"""
import os
@@ -27,9 +31,16 @@ def write_seeds(subdir, seeds):
# ── helpers ──────────────────────────────────────────────────────────────────
-def ws_seed(has_cred, cred, json_msg):
- """websocket_auth format: [has_credential:1][cred_len:2 BE][credential][json]"""
- return struct.pack(">B", has_cred) + struct.pack(">H", len(cred)) + cred + json_msg
+def ws_seed(flags, cred, path, origin, host, ws_msg):
+ """websocket_auth format:
+ [flags:1][cred_len:2 BE][cred][path_len:1][path]
+ [origin_len:1][origin][host_len:1][host][ws_message]"""
+ return (struct.pack(">B", flags)
+ + struct.pack(">H", len(cred)) + cred
+ + struct.pack(">B", len(path)) + path
+ + struct.pack(">B", len(origin)) + origin
+ + struct.pack(">B", len(host)) + host
+ + ws_msg)
def http_seed(mode, cred, path, auth=b""):
@@ -41,35 +52,108 @@ def http_seed(mode, cred, path, auth=b""):
# ── websocket_auth seeds ─────────────────────────────────────────────────────
+# Flags: 0x01=has_cred, 0x02=auth_header, 0x04=check_origin, 0x08=url_arg, 0x10=writable
+
+WS = b"/ws"
+ORIGIN = b"http://localhost:7681"
+HOST = b"localhost:7681"
+JSON_AUTH = b'{"columns":80,"rows":24,"AuthToken":"admin"}'
+JSON_DIMS = b'{"columns":80,"rows":24}'
ws_seeds = {
- "noauth_normal": ws_seed(0, b"", b'{"columns":80,"rows":24}'),
- "noauth_large_term": ws_seed(0, b"", b'{"columns":200,"rows":50}'),
- "noauth_empty_json": ws_seed(0, b"", b"{}"),
- "noauth_with_token": ws_seed(0, b"", b'{"columns":80,"rows":24,"AuthToken":"ignored"}'),
- "auth_valid": ws_seed(1, b"admin", b'{"columns":80,"rows":24,"AuthToken":"admin"}'),
- "auth_invalid": ws_seed(1, b"admin", b'{"columns":80,"rows":24,"AuthToken":"wrong"}'),
- "auth_missing_token": ws_seed(1, b"admin", b'{"columns":80,"rows":24}'),
- "auth_no_dimensions": ws_seed(1, b"admin", b'{"AuthToken":"admin"}'),
- "auth_empty_cred": ws_seed(1, b"", b'{"columns":80,"rows":24}'),
- "json_malformed": ws_seed(0, b"", b"{invalid json}"),
- "json_truncated": ws_seed(0, b"", b'{"columns":'),
- "json_empty": ws_seed(0, b"", b""),
- "json_array": ws_seed(1, b"foo", b"[1,2,3]"),
- "json_huge_values": ws_seed(0, b"", b'{"columns":99999,"rows":99999}'),
- "json_negative": ws_seed(0, b"", b'{"columns":-1,"rows":-1}'),
- "json_uint16_max": ws_seed(0, b"", b'{"columns":65535,"rows":65535}'),
- "json_uint16_overflow":ws_seed(0, b"", b'{"columns":65536,"rows":65536}'),
- "json_extra_ws": ws_seed(0, b"", b'{ "columns" : 80 , "rows" : 24 }'),
- "json_deep_nested": ws_seed(0, b"", b'{"a":' * 50 + b"1" + b"}" * 50),
- "json_huge_token": ws_seed(1, b"admin",
- b'{"columns":80,"rows":24,"AuthToken":"' + b"A" * 5000 + b'"}'),
- "auth_token_bool": ws_seed(1, b"admin", b'{"columns":80,"rows":24,"AuthToken":true}'),
- "auth_token_null": ws_seed(1, b"admin", b'{"columns":80,"rows":24,"AuthToken":null}'),
- "auth_token_int": ws_seed(1, b"admin", b'{"columns":80,"rows":24,"AuthToken":12345}'),
- "auth_token_dup": ws_seed(1, b"admin", b'{"AuthToken":"wrong","AuthToken":"admin"}'),
- "auth_long_cred": ws_seed(1, b"A" * 200,
- b'{"AuthToken":"' + b"A" * 200 + b'"}'),
+ # ── No auth, basic JSON_DATA command ──
+ "noauth_json": ws_seed(0x10, b"", WS, b"", b"", JSON_DIMS),
+ "noauth_json_large": ws_seed(0x10, b"", WS, b"", b"", b'{"columns":200,"rows":50}'),
+ "noauth_json_empty": ws_seed(0x10, b"", WS, b"", b"", b"{}"),
+ "noauth_json_malformed":ws_seed(0x10, b"", WS, b"", b"", b"{invalid json}"),
+
+ # ── With credential — JSON_DATA auth flow ──
+ "auth_valid": ws_seed(0x11, b"admin", WS, b"", b"", JSON_AUTH),
+ "auth_invalid": ws_seed(0x11, b"admin", WS, b"", b"",
+ b'{"columns":80,"rows":24,"AuthToken":"wrong"}'),
+ "auth_missing_token": ws_seed(0x11, b"admin", WS, b"", b"", JSON_DIMS),
+ "auth_no_dims": ws_seed(0x11, b"admin", WS, b"", b"", b'{"AuthToken":"admin"}'),
+ "auth_empty_cred": ws_seed(0x01, b"", WS, b"", b"", JSON_DIMS),
+ "auth_token_null": ws_seed(0x11, b"admin", WS, b"", b"",
+ b'{"columns":80,"rows":24,"AuthToken":null}'),
+ "auth_token_bool": ws_seed(0x11, b"admin", WS, b"", b"",
+ b'{"columns":80,"rows":24,"AuthToken":true}'),
+ "auth_long_cred": ws_seed(0x11, b"A" * 200, WS, b"", b"",
+ b'{"AuthToken":"' + b"A" * 200 + b'"}'),
+
+ # ── Custom auth header (bit 1) ──
+ "custom_auth": ws_seed(0x12, b"someuser", WS, b"", b"", JSON_DIMS),
+ "custom_auth_empty": ws_seed(0x02, b"", WS, b"", b"", JSON_DIMS),
+
+ # ── INPUT command ('0' prefix) ──
+ "input_hello": ws_seed(0x10, b"", WS, b"", b"", b"0hello world"),
+ "input_ctrl_c": ws_seed(0x10, b"", WS, b"", b"", b"0\x03"),
+ "input_long": ws_seed(0x10, b"", WS, b"", b"", b"0" + b"A" * 4096),
+ "input_not_writable": ws_seed(0x00, b"", WS, b"", b"", b"0hello"),
+
+ # ── RESIZE command ('1' prefix) ──
+ "resize_normal": ws_seed(0x10, b"", WS, b"", b"",
+ b'1{"columns":120,"rows":40}'),
+ "resize_huge": ws_seed(0x10, b"", WS, b"", b"",
+ b'1{"columns":65535,"rows":65535}'),
+ "resize_negative": ws_seed(0x10, b"", WS, b"", b"",
+ b'1{"columns":-1,"rows":-1}'),
+
+ # ── PAUSE/RESUME commands ──
+ "pause": ws_seed(0x10, b"", WS, b"", b"", b"2"),
+ "resume": ws_seed(0x10, b"", WS, b"", b"", b"3"),
+
+ # ── Unknown command ──
+ "unknown_cmd": ws_seed(0x10, b"", WS, b"", b"", b"Xunknown"),
+
+ # ── Origin checking (bit 2) ──
+ "origin_match": ws_seed(0x14, b"", WS, ORIGIN, HOST, JSON_DIMS),
+ "origin_mismatch": ws_seed(0x14, b"", WS, b"http://evil.com", HOST, JSON_DIMS),
+ "origin_no_host": ws_seed(0x14, b"", WS, ORIGIN, b"", JSON_DIMS),
+ "origin_no_origin": ws_seed(0x14, b"", WS, b"", HOST, JSON_DIMS),
+ "origin_long": ws_seed(0x14, b"", WS,
+ b"http://" + b"A" * 200 + b":9999",
+ b"A" * 200 + b":9999", JSON_DIMS),
+ "origin_port_80": ws_seed(0x14, b"", WS,
+ b"http://localhost:80", b"localhost", JSON_DIMS),
+ "origin_port_443": ws_seed(0x14, b"", WS,
+ b"https://localhost:443", b"localhost", JSON_DIMS),
+
+ # ── URL args (bit 3) ──
+ "url_arg_enabled": ws_seed(0x18, b"", WS, b"", b"", b"0echo test"),
+ "url_arg_long": ws_seed(0x18, b"", WS, b"", b"", b"0" + b"X" * 250),
+
+ # ── Path variations ──
+ "path_wrong": ws_seed(0x10, b"", b"/wrong", b"", b"", JSON_DIMS),
+ "path_empty": ws_seed(0x10, b"", b"", b"", b"", JSON_DIMS),
+ "path_traversal": ws_seed(0x10, b"", b"/../ws", b"", b"", JSON_DIMS),
+ "path_long": ws_seed(0x10, b"", b"/" + b"A" * 200, b"", b"", JSON_DIMS),
+ "path_null": ws_seed(0x10, b"", b"/ws\x00extra", b"", b"", JSON_DIMS),
+
+ # ── Auth + origin combined ──
+ "auth_origin_valid": ws_seed(0x15, b"admin", WS, ORIGIN, HOST, JSON_AUTH),
+ "auth_origin_invalid": ws_seed(0x15, b"admin", WS, b"http://evil.com", HOST, JSON_AUTH),
+
+ # ── Empty / minimal ──
+ "empty_msg": ws_seed(0x10, b"", WS, b"", b"", b""),
+ "minimal": ws_seed(0x00, b"", b"", b"", b"", b""),
+
+ # ── JSON edge cases ──
+ "json_deep_nested": ws_seed(0x10, b"", WS, b"", b"",
+ b'{"a":' * 50 + b"1" + b"}" * 50),
+ "json_huge_token": ws_seed(0x11, b"admin", WS, b"", b"",
+ b'{"columns":80,"rows":24,"AuthToken":"' + b"A" * 5000 + b'"}'),
+ "json_truncated": ws_seed(0x10, b"", WS, b"", b"", b'{"columns":'),
+ "json_extra_ws": ws_seed(0x10, b"", WS, b"", b"",
+ b'{ "columns" : 80 , "rows" : 24 }'),
+ "json_uint16_overflow": ws_seed(0x10, b"", WS, b"", b"",
+ b'{"columns":65536,"rows":65536}'),
+
+ # ── Authenticated commands (need JSON_DATA first to auth) ──
+ # Note: without auth, non-JSON_DATA commands with credential set get rejected
+ "unauth_input": ws_seed(0x11, b"admin", WS, b"", b"", b"0hello"),
+ "unauth_resize": ws_seed(0x11, b"admin", WS, b"", b"",
+ b'1{"columns":80,"rows":24}'),
}
| | |