java-topology/defects/haproxy/patch/haproxy-0004-http-capture-headers-O-HxC.md
russell@unturf.com ba818693db nmap-0002 + haproxy-0004 + nginx-0004 + weechat-0003 + zeek-0002 + curl-0004: 6 new CWE-407 defects in network tools; count 693→699
nmap-0002:     nmap.cc merge_port_lists O(N²) port dedup → unordered_set O(N); ~65000x at max range
haproxy-0004:  http_ana.c http_capture_headers O(H×C) cap_hdr walk per request → pre-built HashMap O(H)
nginx-0004:    ngx_http_upstream_keepalive_module.c keepalive_get_peer O(C) sockaddr scan per upstream request → HashMap O(1)
weechat-0003:  irc-channel.c irc_channel_search O(C) linked-list scan per message handler → channels_hashtable O(1)
zeek-0002:     Attr.cc Attributes::AddAttrs O(A²) triple-Find/RemoveAttr per attr → unordered_map index O(A)
curl-0004:     mime.c search_header O(P×H) 3x per part per mime_add_headers → pre-indexed header name set O(P)
2026-03-29 22:22:11 -04:00

3.3 KiB
Raw Blame History

haproxy-0004 — http_capture_headers O(H×C) per-request header capture scan

Ecosystem

haproxy (C)

Severity

MEDIUM — hot path: called for every HTTP request and response with capture request header or capture response header directives configured

Location

src/http_ana.c

  • Function: http_capture_headers (~line 5096)
  • Inner loop: for (h = cap_hdr; h; h = h->next) at line 5113

Description

http_capture_headers is called on every HTTP request and response when header capture is configured. It iterates over all H headers in the HTX message, and for each header performs a linear O(C) walk of the cap_hdr linked list to check for a name match:

static void http_capture_headers(struct htx *htx, char **cap, struct cap_hdr *cap_hdr)
{
    for (pos = htx_get_first(htx); pos != -1; pos = htx_get_next(htx, pos)) {
        // ... get header name n ...
        for (h = cap_hdr; h; h = h->next) {         // O(C) per header
            if (h->namelen && (h->namelen == n.len) &&
                (strncasecmp(n.ptr, h->name, h->namelen) == 0)) {
                // capture value
            }
        }
    }
}

Total cost per request: O(H × C) where:

  • H = number of headers in the request/response (typically 10-50, up to 100+)
  • C = number of configured capture headers (can be dozens with complex configs)

Since this runs on every HTTP request+response, it compounds:

  • Large H (100 headers) × large C (50 capture directives) = 5000 strncasecmp calls per request

Fix

Build a hash map from cap_hdr name → cap_hdr * at config parse time. On each request, look up each header name in O(1):

--- a/include/haproxy/proxy-t.h
+++ b/include/haproxy/proxy-t.h
@@ -437,6 +437,8 @@
    struct cap_hdr *req_cap;    /* chained list of request headers to be captured */
    struct cap_hdr *rsp_cap;    /* chained list of response headers to be captured */
+   struct eb_root req_cap_tree;  /* name→cap_hdr for O(log C) lookup */
+   struct eb_root rsp_cap_tree;  /* name→cap_hdr for O(log C) lookup */

--- a/src/http_ana.c
+++ b/src/http_ana.c
@@ -5096,15 +5096,14 @@
 static void http_capture_headers(struct htx *htx, char **cap, struct cap_hdr *cap_hdr)
 {
-   struct cap_hdr *h;
    int32_t pos;

    for (pos = htx_get_first(htx); pos != -1; pos = htx_get_next(htx, pos)) {
        struct htx_blk *blk = htx_get_blk(htx, pos);
        if (htx_get_blk_type(blk) == HTX_BLK_EOH) break;
        if (htx_get_blk_type(blk) != HTX_BLK_HDR) continue;

        struct ist n = htx_get_blk_name(htx, blk);

-       for (h = cap_hdr; h; h = h->next) {          // O(C) walk eliminated
+       struct cap_hdr *h = cap_hdr_lookup(cap_hdr, n); // O(1) hash lookup
+       if (h) {
            if (h->namelen && /* ... match ... */) {
                // capture value
            }
        }
    }
}

Complexity

Variant Cost
Before O(H × C) per request+response pair
After O(H) per request+response pair — O(1) hash lookup per header
Speedup C× (number of capture headers)

Notes

  • With C=1 (one capture directive) this is trivially already O(H); the defect matters when C grows with complex proxy configurations
  • The cap_hdr list is immutable after config parse, making it safe to index
  • HAProxy already uses various tree structures (ceb-trees) for similar purposes