etc.git


etc

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

commit 7dc2f6595696ca2b5e48d3dd401a5952db09d6ec
parent 31d78f4a3c6c607c23e4c4df9c37fc0431deec44
Author: djnn <[email protected]>
Date: Wed, 18 Feb 2026 21:12:57 +0900

feat(ttyd): improve websocket-auth fuzzer;D

Diffstat:
Mfuzzing/ttyd/fuzz_lws_mock.c | 109++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mfuzzing/ttyd/fuzz_websocket_auth.c | 199++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mfuzzing/ttyd/gen_corpus.py | 148++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------

3 files changed, 353 insertions(+), 103 deletions(-)

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}'),
 }