HexBrain

curl · Report #3741135

Connection reuse ignores haproxyprotocol and HAPROXY_CLIENT_IP settings, allowing PROXY context to persist across transfers

N/Astatus
22upvotes
19 May 2026disclosed

How it was found

Summary:

libcurl's connection pool match logic does not include the CURLOPT_HAPROXYPROTOCOL setting or the CURLOPT_HAPROXY_CLIENT_IP value in its connection match key. Two transfers issued through the same Curl_easy or via a shared connection cache (CURL_LOCK_DATA_CONNECT) therefore share one TCP connection even when their PROXY protocol settings differ. The HAProxy PROXY header is sent only once at connect time (cf-haproxy.c HAPROXY_INIT → HAPROXY_SEND → HAPROXY_DONE state machine); once the filter is HAPROXY_DONE on a cached connection, no subsequent transfer on that connection re-issues the header. A later transfer that did not request the PROXY header, or that requested a different spoofed client IP, is still attributed by the backend to the address advertised in the first transfer.

This is one missing connection-match gate producing two manifestations on the same defect:

  • Vector A: PROXY attribution from one transfer is silently inherited by subsequent transfers that did not request it.
  • Vector B: A new spoofed client IP requested by a later transfer is silently dropped because the PROXY header is not re-sent on the reused connection.

The two vectors are one finding at one severity tier — I do not sum them. Same architectural shape as the 2026 connection-reuse CVE family (CVE-2026-4873 TLS requirement; CVE-2026-5545 HTTP Negotiate auth identity; CVE-2026-5773 SMB share; CVE-2026-6429 netrc credentials; CVE-2026-6253 proxy credentials across redirect; CVE-2026-7168 cross-proxy Digest auth state; CVE-2026-3784 proxy credentials in CONNECT reuse): a connection-level setting controls filter chain composition or connection identity but is not snapshotted on the connectdata for comparison at reuse time. HAPROXYPROTOCOL is the unfixed member of that family.

Verified by:

  • grep -n haproxyprotocol lib/url.c on master returns zero matches (no reference in the connection match orchestration).
  • L1 reproduction in docker against curl 8.21.0-DEV (built from master HEAD on 2026-05-17) with an HAProxy 3.2.19 accept-proxy listener and a Python http.server backend; four transfers via two easy handles sharing CURL_LOCK_DATA_CONNECT; all four reuse a single TCP connection (HAProxy frontend-counter fc=1); the HAProxy log column attributed_src=1.2.3.4 is preserved across transfers, including the ones that requested no PROXY context and the one that requested a different spoofed IP.

Affected version

Verified on master HEAD as of 2026-05-17 (curl 8.21.0-DEV). The relevant code paths are unchanged in the 8.20.0 release and have been present since CURLOPT_HAPROXYPROTOCOL was introduced.

Test build curl -V output:

curl 8.21.0-DEV (aarch64-unknown-linux-gnu) libcurl/8.21.0-DEV OpenSSL/3.0.20 zlib/1.2.13 libidn2/2.3.3 libpsl/0.21.2 nghttp2/1.52.0
Release-Date: [unreleased]
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns mqtt mqtts pop3 pop3s rtsp smtp smtps telnet tftp ws wss
Features: alt-svc AsynchDNS Debug HSTS HTTP2 HTTPS-proxy IDN IPv6 Largefile libz PSL SSL threadsafe TLS-SRP UnixSockets

Build environment: debian:bookworm-slim container; deps build-essential autoconf automake libtool pkg-config libssl-dev libnghttp2-dev zlib1g-dev libidn2-dev libpsl-dev; configured with --with-openssl --with-nghttp2 --with-libpsl --enable-debug --disable-shared.

Steps To Reproduce:

The attached files set up a self-contained reproducible PoC. All commands assume the attached tarball is extracted into a working directory.

  1. Build a debug curl from master HEAD inside a container (see README.md for the exact one-line docker run command; produces build-output/install/bin/curl and build-output/install/lib/libcurl.a).

  2. Bring up the HAProxy accept-proxy listener + Python http.server backend:

    cd pocs/haproxy-bypass
    docker compose up -d
    
  3. Compile the attached poc.c against the debug libcurl and run it on the compose network (full command in README.md):

    docker run --rm --network haproxy-bypass_poc_net \
        -v "$(pwd)/build-output:/build" \
        -v "$(pwd)/pocs/haproxy-bypass:/poc" \
        -w /poc debian:bookworm-slim sh -c \
        'apt-get update >/dev/null && apt-get install -y --no-install-recommends \
           gcc libc6-dev libssl-dev libnghttp2-dev zlib1g-dev libidn2-dev libpsl-dev \
           libssl3 libnghttp2-14 libidn2-0 libpsl5 zlib1g >/dev/null && \
         gcc -Wall -O0 -g -o /poc/poc /poc/poc.c \
           -I/build/install/include -L/build/install/lib \
           -lcurl -lssl -lcrypto -lnghttp2 -lz -lidn2 -lpsl -lpthread && \
         LD_LIBRARY_PATH=/build/install/lib /poc/poc http://haproxy:3128/test'
    
  4. Inspect HAProxy logs:

    docker compose logs haproxy --tail=10
    

Expected output (← THIS IS WHERE THE BUG FIRES) — attached verbatim in evidence/haproxy.log:

tag=A attributed_src=1.2.3.4 fc=1 req=GET /test
tag=B attributed_src=1.2.3.4 fc=1 req=GET /test
tag=C attributed_src=1.2.3.4 fc=1 req=GET /test
tag=D attributed_src=1.2.3.4 fc=1 req=GET /test

The four transfer tags correspond to:

  • A: CURLOPT_HAPROXYPROTOCOL=1, CURLOPT_HAPROXY_CLIENT_IP="1.2.3.4"
  • B: CURLOPT_HAPROXYPROTOCOL=0
  • C: CURLOPT_HAPROXYPROTOCOL=1, CURLOPT_HAPROXY_CLIENT_IP="9.8.7.6"
  • D: CURLOPT_HAPROXYPROTOCOL=0

Frontend connection counter fc=1 for all four transfers demonstrates a single TCP connection is reused. Backend log (attached evidence/backend.log) shows the same backend peer port 46398 for all four transfers, confirming TCP-level reuse. The libcurl wire trace (attached evidence/curl-wire-trace.txt) shows Connection #0 to host haproxy:3128 left intact after each transfer. The HAProxy attributed_src column is 1.2.3.4 for all four transfers, including:

  • Transfer B, which sent no PROXY header — backend attributes it to A's spoofed IP.
  • Transfer C, which requested a different spoofed IP (9.8.7.6) — the PROXY header is not re-sent because the filter is in HAPROXY_DONE state on the cached connection; backend continues to attribute to 1.2.3.4.

Related code (verbatim references on master HEAD)

  • lib/urldata.h struct UserDefined: BIT(haproxyprotocol), STRING_HAPROXY_CLIENT_IP.
  • lib/connect.c:429: filter chain insertion gated on data->set.haproxyprotocol at connection setup.
  • lib/cf-haproxy.c: PROXY header is sent once when the filter transitions through HAPROXY_INIT → HAPROXY_SEND → HAPROXY_DONE, never re-sent on reused connections.
  • lib/url.c::url_match_connect_config() (around line 694) and lib/url.c::url_match_conn() (around line 1129): connection match orchestration. Neither references haproxyprotocol nor STRING_HAPROXY_CLIENT_IP.

Architectural pattern — this is the unfixed member of the 2026 connection-reuse family

Across the recent 2026 connection-reuse CVE family in curl, fixes share one structural pattern: snapshot the connection-level setting on connectdata at allocate time and compare in the match function. (CVE-2026-3805 is excluded from this table because it is a memory-safety bug — use-after-free — triggered by SMB connection reuse, not a missing-match-key bug.)

Fixed inMissing match fieldPatch idiom
CVE-2026-3784Proxy credentials in CONNECT reuseAdd credentials compare in match
CVE-2026-4873TLS requirement on reuseAdd TLS-requirement compare in match
CVE-2026-5545HTTP Negotiate auth identityAdd credential-identity compare in match
CVE-2026-5773SMB share identifierDisable SMB connection reuse entirely
CVE-2026-6253Proxy credentials across redirect-to-proxyAdd scope compare in match
CVE-2026-6429netrc credentials with reused proxyAdd host-scope compare in match
CVE-2026-7168Cross-proxy Digest auth stateReset auth state on proxy change
(this report)CURLOPT_HAPROXYPROTOCOL + CURLOPT_HAPROXY_CLIENT_IPSame idiom — add compare in match

Seven family members already share the same fix idiom; HAPROXYPROTOCOL is the unfixed member. The defect surface is internally consistent: one connection-level user setting whose value controls filter-chain composition or connection identity, omitted from the match function.

Severity stance

I do not assign a severity tier and defer to the curl security team. For reference, CVE-2026-4873 (TLS requirement ignored on reuse — closest structural sibling) was rated Low. I explicitly do NOT claim High or Critical, and I do NOT sum Vector A and Vector B into a higher tier — the two manifestations stem from one missing match gate on one decision site (the connection match orchestrator in lib/url.c) and are packaged as one finding at one tier.

Again: there is no bug-bounty for curl. We do not offer any rewards, only proper credits.

Impact

Summary:

Where an application reuses libcurl connections across transfers with different HAProxy PROXY protocol settings (via the CURL_LOCK_DATA_CONNECT share interface or the default per-handle connection pool), the backend observes PROXY context from the first transfer applied to subsequent transfers. Where the backend treats the PROXY-derived source address as a security boundary, two distinct abnormal states result:

  1. A spoofed source IP from one transfer is silently inherited by later transfers that did not request PROXY spoofing at all. If the backend uses the PROXY-derived source IP for IAM policy decisions (for example AWS IAM aws:SourceIp condition keys), WAF IP rules, per-IP rate-limit buckets, or audit-log correlation, those later transfers operate under a different attribution than the caller requested.

  2. A new spoofed source IP requested by a later transfer is silently dropped — the PROXY header is not re-sent on a reused connection, so the backend continues to see the earlier IP. The caller's intent to change attribution is lost without any surfacing error from libcurl.

Who can exploit

Any caller within a process that shares a libcurl connection pool across PROXY-protocol settings. This is the normal operating mode in deployments where one process embeds libcurl on behalf of multiple callers — API gateways, embedded proxy libraries, Kubernetes sidecar HTTP clients, serverless function wrappers, CI/CD runner agents. In single-tenant CLI use the precondition is unusual; the realistic attack surface is multi-tenant or multi-context library embedding.

Affected population

Any libcurl-using application that both (a) issues some transfers with --haproxy-protocol / CURLOPT_HAPROXYPROTOCOL=1, and (b) issues other transfers without it (or with a different CURLOPT_HAPROXY_CLIENT_IP), and reuses the connection pool across them. HAProxy v1 PROXY protocol client support has been in libcurl since CURLOPT_HAPROXYPROTOCOL was introduced; the connection-reuse field-mismatch path has been the source of seven fixed 2026 CVEs in curl 8.19.0 and 8.20.0 (see Description for the precedent table), and this is one more instance.

Persistence

The cached connection holds the first transfer's PROXY context for the lifetime of the connection (HTTP keep-alive idle timeout, typically 60–300 seconds in deployment). The next transfer that pays the cost of opening a new TCP connection clears the state; until then, every subsequent transfer on that socket inherits the first transfer's attribution.

Detectability

No client-side error or warning is surfaced. The PROXY header simply is not re-sent; the backend cannot distinguish "intentional re-use of attribution" from "PROXY context silently inherited from a previous transfer". Audit detectability depends on the backend's per-transfer logging granularity and on whether the backend logs the resolved source IP per request.

Severity floor (empirically anchored, not hedged)

I defer to the curl security team for severity assignment. Recent connection-reuse field-mismatch bugs in this family have historically been rated in the Low–Medium range by the curl team (see Description for the seven 2026 precedents). I have not collected evidence of an end-to-end production exploit chain (IAM bypass against a live AWS account, WAF rate-limit bypass against a live deployment) and do not assert such evidence; the report rests on the structural defect plus the empirical reproduction of attribution preservation across transfer boundaries.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original