The ScorpioX codebase is a large, multi-component C project comprising a terminal multiplexer, email server (SMTP/IMAP), HTTP server, AI provider clients, a KVM-based VM hypervisor, file transfer utilities, and more. The project demonstrates good security awareness in many areas — using snprintf over sprintf in 2,172 out of 2,192 format calls, performing bounds-checked header parsing in the HTTP server, and sanitizing filenames in network receive paths. However, critical and high-severity vulnerabilities exist, primarily in shell command injection (232 system()/popen() calls, many with insufficiently sanitized input), missing null checks after allocation (166 instances), and 20 unbounded sprintf calls that can cause heap/stack buffer overflows. The email server and HTTP server — both network-facing — have the highest exposure.
| # | Severity | CVSS | Location | Category | Description |
|---|---|---|---|---|---|
| V-01 | CRITICAL | 9.8 | sxmail_maildir.c:704 | Command Injection | system("rm -rf ' — single-quote in folder name = arbitrary shell command execution |
| V-02 | CRITICAL | 9.8 | scorpiox-vm.c:488-493 | Command Injection | system(cmd) with URL/path from user config passed through curl/wget — single-quote injection |
| V-03 | CRITICAL | 9.1 | sx.c:1318,1575,2109,2262,2458 | Command Injection | Multiple system() calls building commands from session data, config values, file paths |
| V-04 | CRITICAL | 9.1 | scorpiox-agent.c (12 calls) | Command Injection | system() and popen() with agent-provided paths, find -exec with unsanitized directory names |
| V-05 | HIGH | 8.1 | libsxnet/sx_http_wasm.c:388-460 | Buffer Overflow | sprintf chain into static char json[32768] — escape expansion (\uXXXX) can exceed needed estimate |
| V-06 | HIGH | 8.1 | libsxnet/sx_api.c:367-404 | Buffer Overflow | sprintf chain into heap buffer — size estimate uses +16 per field but sprintf format overhead can exceed this |
| V-07 | HIGH | 7.8 | scorpiox-wsl.c:1278-1279 | Buffer Overflow | strcat(command_buf, argv[i]) in loop into char command_buf[4096] — no bounds checking |
| V-08 | HIGH | 7.8 | scorpiox-traffic.c:926-927 | Buffer Overflow | strcat(cmdline, argv[i]) in loop into char cmdline[4096] — no bounds checking |
| V-09 | HIGH | 7.5 | 166 locations (see §5) | Null Deref | malloc/calloc/realloc return values used without null checks — crash on OOM |
| V-10 | HIGH | 7.5 | libsxnet/sx_mcp.c:701 | Null Deref + Overflow | realloc(buf, cap) in loop with cap = 2 — no null check, potential integer overflow on cap |
| V-11 | HIGH | 7.5 | libsxnet/sx_http.c:44 | Integer Overflow | size nmemb multiplication in curl write callback — can overflow size_t on 32-bit platforms |
| V-12 | MEDIUM | 6.5 | libsxnet/sx_http_wasm.c:39,303,388,639 | Thread Safety | Multiple static char buffers (up to 65536 bytes) — data races if called from multiple threads |
| V-13 | MEDIUM | 6.5 | sx_dll.c:288 | Fixed Buffer | char enqueue_buf[8192] — shared across agent messages, no overflow check on concatenation |
| V-14 | MEDIUM | 6.1 | sx_term.c:1840 | Buffer Overflow | sprintf(osc, ...) with b64 string — safe (allocated as b64_size + 20) but relies on exact math |
| V-15 | MEDIUM | 5.9 | scorpiox-server.c:1233 | Input Validation | Content-Length parsed via strtoll without upper-bound validation — can cause huge allocations |
| V-16 | MEDIUM | 5.5 | 20 sprintf calls | Buffer Overflow | Unbounded sprintf calls (see §2 for full list) — heap or stack corruption risk |
| V-17 | MEDIUM | 5.3 | sxmail_smtp.c:365 | Missing Null Check | realloc for SMTP DATA accumulation — null return causes use-after-free of old buffer |
| V-18 | MEDIUM | 5.3 | scorpiox-unshare.c:140 | Buffer Overflow | strcpy(buf + dlen + 1, prog) — no bounds check on prog length vs remaining buffer space |
| V-19 | LOW | 4.0 | sx_term.c:515-518 | Signal Safety | SIGWINCH handler sets g_term.resized = true — safe (signal-flag pattern), but signal() used instead of sigaction() |
| V-20 | LOW | 3.7 | 73 compiler warnings | Ignored Returns | system(), read(), write(), chdir(), symlink() return values ignored — silent failures |
| V-21 | LOW | 3.3 | scorpiox-unshare.c:124-141 | TOCTOU | access() check then execve() — time-of-check-time-of-use race on file existence |
| V-22 | LOW | 3.0 | 28 strcpy calls | Bounded Copy | Most strcpy calls copy string literals or bounded values — low risk but should use snprintf |
| V-23 | INFO | 2.0 | scorpiox-whatsapp.c:156 | Input Parsing | sscanf for version parsing — bounded by format but lacks input length validation |
sprintf Calls (20 instances)The codebase contains 20 sprintf calls without bounds checking. While the project predominantly uses snprintf (2,172 calls), the remaining sprintf calls represent real overflow risks:
sprintf chains:
| File | Line | Pattern | Risk |
|---|---|---|---|
sx_http_wasm.c | 394-451 | p += sprintf(p, ...) chain building JSON into static char[32768] | The needed calculation uses *2 for escaping but \uXXXX expands 1→6 chars (6x). If body contains many control characters, actual expansion exceeds estimate. The if (needed > sizeof(json)) check uses the underestimate. |
sx_api.c | 367-404 | offset += sprintf(buf + offset, ...) chain | Buffer total calculation uses +16 per field for format overhead, but sprintf("%zu:", len) for large len values can produce 20+ digit strings. Multiple fields compound the undercount. |
sx_slashcmd.c | 1118 | sprintf(p, "- %s: ", name) | No remaining-space tracking in loop building description string |
sx_exec_wasm_builtins.c | 1778-1781 | sprintf(new_json, "[%s]", entry) / sprintf(new_json + len - 1, ",%s]", entry) | entry could be arbitrarily large |
scorpiox-mcp.c | 1192 | sprintf(out, "Error: %s", estr) | If estr exceeds out buffer |
sprintf calls with snprintf and check return values.
strcpy and strcat (37 instances)strcat in loops:
// scorpiox-wsl.c:1278-1279 — argv concatenation into fixed buffer
char command_buf[4096] = {0};
for (int i = arg_start + 1; i < argc; i++) {
if (command_buf[0]) strcat(command_buf, " ");
strcat(command_buf, argv[i]); // NO BOUNDS CHECK
}
// scorpiox-traffic.c:926-927 — same pattern
char cmdline[4096] = {0};
for (int i = 0; i < argc; i++) {
if (i > 0) strcat(cmdline, " ");
strcat(cmdline, argv[i]); // NO BOUNDS CHECK
}
Long argument lists will overflow these 4096-byte stack buffers, corrupting the return address.
High-riskstrcpy:
// scorpiox-unshare.c:140
strcpy(buf + dlen + 1, prog); // prog length unchecked
// scorpiox-agent.c:1452
strcpy(out + len - 4, ".dll"); // assumes len >= 4
The codebase uses 2,311 stack-allocated char buffers. Notable large allocations:
sxmail_dkim.c:414 — char sign_data[32768] (32KB on stack)sx_dll.c:288 — char enqueue_buf[8192] (in global struct)sxmail_imap.c:927 — char resp[8192]sxmail_queue.c:896 — char bounce[8192]Stack exhaustion risk on deeply nested calls with 8KB+ buffers per frame.
No classic format string vulnerabilities were found (i.e., printf(user_input) without format string). All printf/fprintf calls use string literals as format specifiers. This is well-implemented.
The snprintf calls that build command strings for system() can indirectly introduce format-related issues if the resulting command string is interpreted by the shell. This is primarily a command injection risk (see §6).
// sx_http.c:44 — curl write callback
size_t real_size = size * nmemb; // Can overflow on 32-bit
While curl guarantees size == 1, the contract allows arbitrary values. On 32-bit platforms, size * nmemb can wrap to a small value, causing subsequent memcpy to write beyond the buffer.
// sx_http.c:48 — doubling strategy
size_t newcap = buf->cap * 2; // Can overflow to 0
If cap is near SIZE_MAX/2, doubling wraps to 0 or a small value. The follow-up realloc(buf->data, newcap) with newcap=0 frees the buffer, and subsequent memcpy writes to freed memory.
// sxtmux_backend_sxmux.c:62
int len = (int)strlen(keys); // Truncates on strings > 2GB (theoretical)
// sxmux_session.c:28
int len = (int)strlen(tmp);
19 instances of casting strlen() (returns size_t) to int. On 64-bit systems this truncates for strings > 2GB. While unlikely in practice, this violates C safety invariants.
realloc Doubling Overflow// sx_mcp.c:701
while (len + need >= cap) { cap *= 2; buf = realloc(buf, cap); }
cap *= 2 can overflow to 0 on repeated doublingsrealloc return — old buf is leaked and buf becomes NULLsnprintf(buf + len, ...) dereferences NULLSame pattern in sx_provider_gemini_wasm.c:56, sx_provider_openai.c:323.
malloc/calloc/realloc calls lack null-pointer checks. These will crash on out-of-memory conditions. Key high-risk locations:
| File | Line | Call | Impact |
|---|---|---|---|
sx_http.c | 156 | malloc(8192) — HTTP response buffer | Crash during active HTTP request |
sx_frp.c | 493 | malloc(st->read_buf_size) — FRP read buffer | Crash in network relay |
sx_provider.c | 151 | calloc(1, sizeof(SxProvider)) | Crash on provider init |
sx_api.c | 25, 61 | calloc(SX_INITIAL_HISTORY, ...) | Crash on API init |
sx_bgtask.c | 210 | malloc(SX_BGTASK_OUTPUT_MAX) — background task output | Crash during task creation |
sx_mcp.c | 701 | realloc(buf, cap) — MCP tool list | Null deref + use-after-free |
The realloc pattern without null check is the primary use-after-free risk:
// When realloc fails, it returns NULL but does NOT free the old pointer.
// However, assigning the result to the same variable:
buf = realloc(buf, cap); // If this returns NULL, old buf is leaked
// and buf is now NULL — subsequent use = crash
This pattern appears in:
sx_mcp.c:701sx_provider_gemini_wasm.c:56sx_provider_openai.c:323sx_provider_claude_code.c:580Return paths that skip free():
sx_api.c:367: sprintf chain into buf = malloc(total) — if the function returns early after partial construction, buf is leaked (though no early returns exist currently)goto cleanup idiom)The codebase contains 232 system()/popen()/exec*() calls. This is the most significant attack surface.
Multiple locations use single-quote wrapping to "protect" user input in shell commands:
// sxmail_maildir.c:704 — CRITICAL: email folder name from IMAP client
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", path);
system(cmd); // If path contains: foo'; rm -rf /; echo '
// scorpiox-vm.c:488 — CRITICAL: URL from config
snprintf(cmd, sizeof(cmd), "curl -fSL --progress-bar -o '%s' '%s'", dest, url);
system(cmd); // If url contains single quotes
// scorpiox-unshare.c:427
snprintf(cmd, sizeof(cmd), "tar xf '%s' -C '%s'", tar_path, rootfs_dir);
// scorpiox-server.c:688
snprintf(cmd, sizeof(cmd), "git -C '%s' rev-parse HEAD 2>&1", repo_dir);
Single-quote wrapping is not sufficient — an attacker providing a value like '; malicious_command; echo ' breaks out of the quotes.
// scorpiox-agent.c:1313 — find -exec with agent directory
snprintf(cmd, sizeof(cmd),
"find '%s' -name '*.csproj' -exec grep -li 'scorpiox\\.agent\\.dotnet' {} \\;",
agent_dir);
FILE *fp = popen(cmd, "r");
system() Calls by Component| Component | Count | User Input Path | Risk |
|---|---|---|---|
scorpiox-wsl.c | 12 | Distro names, bind paths, WSL args | High — user-provided distro names flow to system() |
scorpiox-unshare.c | 15 | Rootfs paths, tar paths, package lists | High — paths from config |
scorpiox-agent.c | 8 | Agent directory, hook scripts | Critical — agent-provided paths |
sxmail_maildir.c | 2 | Folder name from IMAP DELETE | Critical — network input |
scorpiox-vm.c | 4 | Image URLs, file paths | High — URLs from user |
sx.c | 8 | Session IDs, file paths, voice commands | Medium — local user input |
scorpiox-server.c | 2 | Git repo paths (CGI) | High — web-accessible |
system()/popen() calls with fork()+execve() with explicit argv arrays, or implement proper shell-escaping functions.
The codebase uses POSIX threads in two primary areas:
sx_dll.c: Worker thread with mutex-protected event queue and input queue — properly synchronizedsx_cache_keepalive.c: Keepalive thread with mutex protection — properly synchronizedBoth use pthread_mutex_lock/unlock consistently. No obvious data races in the mutex-protected sections.
sx_http_wasm.c uses multiple static buffers:
static char sx_xhr_response_buf[65536]; // Line 39
static char json[4096]; // Line 303
static char json[32768]; // Line 388
static char body_copy[65536]; // Line 639
These are in WASM context (single-threaded), so this is low-risk in practice but violates safe coding principles.
// sx_term.c:515
static void term_sigwinch(int sig) {
(void)sig;
g_term.resized = true; // Safe: volatile-style flag pattern
}
The handler only sets a boolean flag — this is the correct async-signal-safe pattern. However, signal() is used instead of sigaction(), which has undefined behavior for re-entrant signals on some platforms.
// scorpiox-unshare.c:124,141
return access(prog, X_OK) == 0;
// ... later ...
execve(buf, ...); // File could have changed between access() and exec()
30+ instances of stat()/access() followed by open()/exec(). Low risk in most contexts (local programs, not setuid) but technically exploitable.
// sxmux_session.c:1558
if (fscanf(pf, "%d", &existing_pid) == 1 && existing_pid > 0) {
// Check if pid is alive, then decide to reuse
// Race: pid could die and be recycled between check and use
}
Compilation with -Wall -Wextra -Wformat-security -Wstack-protector produced 95 unique warnings:
| Warning Type | Count | Security Impact |
|---|---|---|
-Wunused-result | 73 | Medium: Ignored system(), read(), write(), chdir(), symlink(), fread() return values can mask failures silently |
-Wunused-function | 13 | Low: Dead code |
-Wunused-variable | 3 | Info: Minor |
-Wunused-const-variable | 2 | Info: Minor |
-Wswitch | 1 | Low: Unhandled enum case in sx.c:927 (SX_CMDSRC_BUILTIN_CMD) |
-Wtype-limits | 1 | Low: Comparison always true/false |
-Wformat-truncation and -Wno-stringop-truncation in CMakeLists.txt — these may mask legitimate truncation bugs.
Key ignored return values:
system() return in scorpiox-unshare.c (8 instances) — cannot detect failed commandsread() / write() in scorpiox-unshare.c, sx_term.c — potential partial I/Ochdir() in scorpiox-unshare.c — process may operate in wrong directoryThe codebase demonstrates several good security practices:
snprintf dominance: 2,172 of 2,192 format calls use snprintf with sizeof() bounds (99.1%)scorpiox-server.c properly bounds header extraction with sizeof() checksscorpiox-beam.c:755 uses sx_basename() to strip path components from received filenamessxmail_smtp.c enforces SXMAIL_SMTP_MAX_MSG_SIZE on accumulated datasx_http.c write_cb uses doubling-growth realloc with null checks on realloc (partial — the check exists but the overflow on doubling does not)ct_len >= sizeof(req->content_type) before copyprintf-family calls use literal format strings| Risk Level | Count | Findings |
|---|---|---|
| CRITICAL (9.0+) | 4 | V-01, V-02, V-03, V-04 — Shell command injection |
| HIGH (7.0-8.9) | 7 | V-05 through V-11 — Buffer overflows, null derefs, integer overflows |
| MEDIUM (4.0-6.9) | 8 | V-12 through V-19 — Thread safety, input validation, bounded copies |
| LOW (1.0-3.9) | 4 | V-20 through V-23 — Ignored returns, TOCTOU, safe strcpy |
| Total | 23 |
system()/popen() with unsanitized input: Replace all system(snprintf(..., "'%s'", user_input)) patterns with fork()+execve() using explicit argv[] arrays. This eliminates shell interpretation entirely.system() must be used, create sx_shell_escape(const char input, char output, size_t output_size) that escapes all shell metacharacters (', ", ` `, $, \, !, (, ), {, }, |, &, ;, <, >, \n).: This is network-reachable via IMAP DELETE — an attacker with mailbox access can execute arbitrary commands by creating a folder with shell metacharacters in the name. calls with snprintf: Especially the chain patterns in sx_http_wasm.c, sx_api.c, and sx_exec_wasm_builtins.c. #define SX_ALLOC(ptr, size) do { \
(ptr) = malloc(size); \
if (!(ptr)) { SX_ERROR("OOM at %s:%d", __FILE__, __LINE__); abort(); } \
} while(0)
patterns: Never assign realloc() result to the same pointer: char *tmp = realloc(buf, cap);
if (!tmp) { free(buf); return -1; }
buf = tmp;
loops in scorpiox-wsl.c and scorpiox-traffic.c: Use snprintf with remaining-space tracking. with sigaction() for portable, reliable signal handling. against a maximum (e.g., 100MB) in HTTP server before allocating. calls with snprintf(dst, sizeof(dst), "%s", src)., read(), write() returns.+exec() TOCTOU patterns with fexecve() or openat(). from CMakeLists.txt and fix any resulting warnings. to default build flags permanently.) in CI/debug builds for runtime detection.| Function | Occurrences | Safe Alternative |
|---|---|---|
sprintf | 20 | snprintf |
strcpy | 28 | snprintf / strlcpy |
strcat | 9 (functional) | snprintf / strlcat |
sscanf | 15 | Field-width limits |
fscanf | 15 | fgets + sscanf |
fgets | 122 | (safe when used correctly ✓) |
system | ~120 | fork+execve |
popen | ~50 | fork+execve+pipe |
signal | 4 | sigaction |
access | 15 | faccessat / open |
atoi | 3 | strtol with validation |
| File | Lines | Risk Factors |
|---|---|---|
scorpiox-unshare.c | ~2400 | 15 system() calls, TOCTOU, ignored returns, strcpy |
scorpiox-wsl.c | ~1300 | 12 system() calls, strcat overflow, strcpy |
scorpiox-vm.c | ~3149 | system() with URLs, popen(), ioctl() without full error checking |
scorpiox-agent.c | ~2200 | 8 system()/popen() with agent paths, find -exec injection |
sxmail_maildir.c | ~900 | system("rm -rf") with IMAP folder name — network reachable |
scorpiox-server.c | ~2200 | HTTP request parsing, CGI environment, popen() |
sx_dll.c | ~1100 | Threading, fixed enqueue_buf, sprintf |
sx_http_wasm.c | ~1200 | Static buffers, sprintf chains, thread-unsafe globals |
sx_api.c | ~420 | sprintf chain, realloc without null check |
sxmail_smtp.c | ~800 | Network input parsing, realloc` without null check |