java-topology/defects/suricata-0002/TICKET.md

4.1 KiB
Raw Blame History

suricata-0002 — CWE-407 O(H×F) HTTP header field membership scan in Eve JSON logger

Target: Suricata (OISF/suricata, current HEAD) File: src/output-json-http.c Function: EveHttpLogJSONHeaders Severity: MEDIUM MOAD: 0001 (CWE-407 — Inefficient Algorithmic Complexity) Benchmark: ~8x op-count ratio at H=50 headers, F=53 fields (E=1 enabled field)

Defect

EveHttpLogJSONHeaders logs HTTP headers to Eve JSON. When custom field selection is configured (fields != 0), it checks each incoming header against the full 53-entry http_fields[] array on every HTTP transaction:

// src/output-json-http.c:326 — called per HTTP transaction, per direction
for (size_t i = 0; i < n; i++) {                         // O(H) — H headers
    const htp_header_t *h = htp_headers_get_index(headers, i);
    if ((http_ctx->flags & direction) == 0 && http_ctx->fields != 0) {
        bool tolog = false;
        for (HttpField f = HTTP_FIELD_ACCEPT; f < HTTP_FIELD_SIZE; f++) {  // O(F=53)
            if ((http_ctx->fields & (1ULL << f)) != 0) {
                if (bstr_cmp_c_nocase(htp_header_name(h), http_fields[f].htp_field)) {
                    tolog = true;
                    break;
                }
            }
        }
    }
}

Complexity: O(H × F) per transaction, where H = HTTP headers per request (typically 10-50, up to 100+) and F = 53 (HTTP_FIELD_SIZE). A typical request with 30 headers against 53 fields = 1590 string comparisons per transaction. In high-traffic deployments (10k tx/s) this becomes 15.9M string comparisons per second on a single core, all avoidable.

The operator typically enables 1-5 custom fields. Iterating all 53 to find the 1 matching is classic MOAD-0001.

Fix

At configuration time, record only the enabled fields' htp_field strings in a compact enabled_htp_fields[] array on LogHttpFileCtx. At runtime, iterate only the E enabled entries instead of all 53:

// Config time: O(F) once per output context
for (f = HTTP_FIELD_ACCEPT; f < HTTP_FIELD_SIZE; f++) {
    if (field matches) {
        http_ctx->fields |= (1ULL << f);
        http_ctx->enabled_htp_fields[http_ctx->enabled_htp_fields_cnt++] =
            http_fields[f].htp_field;
    }
}

// Runtime: O(H × E) where E = enabled field count (typically 1-5)
for (uint32_t ei = 0; ei < http_ctx->enabled_htp_fields_cnt; ei++) {
    if (bstr_cmp_c_nocase(htp_header_name(h), http_ctx->enabled_htp_fields[ei])) {
        tolog = true; break;
    }
}

With E=1 (one custom field enabled), runtime drops from O(H×53) to O(H×1), ~53x reduction per transaction. With E=5, ~10x reduction. No hash table overhead — the list is tiny and cache-hot.

Complexity table

Scenario Before After Ratio
E=1, H=30 53 iters/header × 30 = 1590 1 × 30 = 30 ~53x
E=3, H=30 53 × 30 = 1590 3 × 30 = 90 ~17x
E=5, H=50 53 × 50 = 2650 5 × 50 = 250 ~10x
E=53, H=50 53 × 50 = 2650 53 × 50 = 2650 1x (no regression)

MOAD 0002-0005 Scan Results

MOAD-0002 (Intertangle): DetectEngineCtx is a large shared context but it is protected by a reload lock (de_ctx->reference / swap) rather than being a god object coupling subsystems at runtime. Subsystems access it through well-defined interfaces. MEDIUM concern but architectural, not a point defect.

MOAD-0003 (Leaked Context): ThreadVars / DecodeThreadVars carry per-thread packet-processing state. No evidence of cross-thread leakage — each worker thread owns its ThreadVars exclusively. Packet and flow pointers are stack-passed, not carried in thread-local storage across request boundaries. CLEAN.

MOAD-0004 (Logged Secret): Previously captured in suricata-0001. authorization and proxy-authorization are registered as loggable via the custom: field list — when a user enables them, credentials are logged verbatim with no redaction. suricata-0001 patch adds a credential denylist.

MOAD-0005 (Thundering Herd): Flow table uses per-flow locks (not global cache-level get+put). FlowGetFlowFromHash acquires the flow lock before returning; no unsynchronized compute-and-insert pattern found. CLEAN.