CVE-2026-3784: wrong proxy connection reuse with credentials
What the report claims
libcurl may reuse an existing HTTP proxy CONNECT tunnel without matching proxy credentials when selecting a reusable connection.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2026-3784 wrong proxy connection reuse with credentials
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The code supports the reported flaw. A maintainer should review its impact and decide what to do next.
Why this finding?
- proxy_info_matches() at lib/url.c:589-595 compares only proxytype, port, and host.name — it omits user and passwd fields
- url_match_proxy_use() at lib/url.c:934 calls proxy_info_matches for HTTP proxy connections, so credential differences are ignored during connection reuse
- socks_proxy_info_matches() at lib/url.c:600-616 explicitly compares user and passwd, demonstrating the intended pattern is missing for HTTP proxy
- Shared connection cache (CURLSH + CURL_LOCK_DATA_CONNECT) is a documented feature; when used, this flaw allows a transfer with invalid proxy credentials to reuse a CONNECT tunnel authenticated with different credentials
- This is a security issue: it bypasses proxy authentication policy in multi-tenant or shared-pool architectures
How this analysis ran 6 claims · 12 citations verified
Checked against curl master on 2026-03-04 (38ee35353c1d). Test evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Review a draft reply to the reporter
Edit and verify before sending.
Thank you for the detailed report. The code confirms the flaw: proxy_info_matches() (lib/url.c:589-595) compares only proxytype, port, and host for HTTP proxy, while socks_proxy_info_matches() (lib/url.c:600-616) correctly compares user and passwd. When a shared connection cache (CURLSH + CURL_LOCK_DATA_CONNECT) is used, url_match_proxy_use() reuses HTTP CONNECT tunnels without checking credentials. This is a valid security issue allowing proxy authentication bypass. We will work on a fix.
Inspect all claims and source code 6 claims · 2 code excerpts
Report claims
proxy_info_matches() compares proxy type, host, and port but does not compare proxy username or password
Read the reasoning
The proxy_info_matches function at lines 589-595 explicitly compares only proxytype, port, and host.name. It does not compare the user or passwd fields of the proxy_info struct.
url_match_proxy_use() calls proxy_info_matches()
Read the reasoning
At line 934, url_match_proxy_use calls proxy_info_matches(&m->needle->http_proxy, &conn->http_proxy) when the httpproxy bit is set.
shared connection cache (CURLSH + CURL_LOCK_DATA_CONNECT)
Read the reasoning
The deterministic checks confirm both CURL_LOCK_DATA_CONNECT and CURLSHOPT_SHARE exist in the codebase, which are the mechanisms for shared connection caches.
a transfer using different proxy credentials can reuse a previously authenticated tunnel
Read the reasoning
Since url_match_proxy_use uses proxy_info_matches (which lacks credential checks) for HTTP proxy connections, and the shared connection cache (CURLSH + CURL_LOCK_DATA_CONNECT) enables connection reuse across transfers, a transfer with different proxy credentials can reuse a previously authenticated HTTP CONNECT tunnel. Notably, socks_proxy_info_matches (lines 600-616) does compare credentials, showing the pattern exists but is missing for HTTP proxy.
second transfer configured with bad:bad succeeds through the existing tunnel without issuing a new CONNECT
Read the reasoning
The reproduction steps and PoC script are not in the provided code. The code supports the mechanism described, but the specific reproduction cannot be verified from source alone.
wrapper/daemon model where multiple clients submit jobs to a process holding the shared connection cache
Read the reasoning
The wrapper/daemon model (curl_wrap_daemon.c) is not in the provided code. The code supports the shared connection cache mechanism that would enable this scenario, but the specific daemon code cannot be verified.
Read the original report (excerpt)
Source code
| 905 | if(!Curl_conn_is_ssl(conn, FIRSTSOCKET)) |
| 906 | return FALSE; |
| 907 | } |
| 908 | else if(Curl_conn_is_ssl(conn, FIRSTSOCKET)) { |
| 909 | /* If the protocol does not allow reuse of SSL connections OR |
| 910 | is of another protocol family, not a match. */ |
| 911 | if(!(m->needle->scheme->flags & PROTOPT_SSL_REUSE) || |
| 912 | (get_protocol_family(conn->scheme) != m->needle->scheme->protocol)) |
| 913 | return FALSE; |
| 914 | } |
| 915 | return TRUE; |
| 916 | } |
| 917 | |
| 918 | #ifndef CURL_DISABLE_PROXY |
| 919 | static bool url_match_proxy_use(struct connectdata *conn, |
| 920 | struct url_conn_match *m) |
| 921 | { |
| 922 | if(m->needle->bits.httpproxy != conn->bits.httpproxy || |
| 923 | m->needle->bits.socksproxy != conn->bits.socksproxy) |
| 924 | return FALSE; |
| 925 | |
| 926 | if(m->needle->bits.socksproxy && |
| 927 | !socks_proxy_info_matches(&m->needle->socks_proxy, &conn->socks_proxy)) |
| 928 | return FALSE; |
| 929 | |
| 930 | if(m->needle->bits.httpproxy) { |
| C2: url_match_proxy_use checks httpproxy bit | |
| 931 | if(m->needle->bits.tunnel_proxy != conn->bits.tunnel_proxy) |
| 932 | return FALSE; |
| 933 | |
| 934 | if(!proxy_info_matches(&m->needle->http_proxy, &conn->http_proxy)) |
| C2: calls proxy_info_matches for http_proxy | |
| C4: url_match_proxy_use uses proxy_info_matches for HTTP proxy | |
| 935 | return FALSE; |
| 936 | |
| 937 | if(IS_HTTPS_PROXY(m->needle->http_proxy.proxytype)) { |
| 938 | /* https proxies come in different types, http/1.1, h2, ... */ |
| 939 | if(m->needle->http_proxy.proxytype != conn->http_proxy.proxytype) |
| 940 | return FALSE; |
| 941 | /* match SSL config to proxy */ |
| 942 | if(!Curl_ssl_conn_config_match(m->data, conn, TRUE)) { |
| 943 | DEBUGF(infof(m->data, |
| 944 | "Connection #%" FMT_OFF_T |
| 945 | " has different SSL proxy parameters, cannot reuse", |
| 946 | conn->connection_id)); |
| 947 | return FALSE; |
| 948 | } |
| 949 | /* the SSL config to the server, which may apply here is checked |
| 950 | * further below */ |
| 951 | } |
| 952 | } |
| 953 | return TRUE; |
| 954 | } |
| 955 | #else |
| 956 | #define url_match_proxy_use(c, m) ((void)(c), (void)(m), TRUE) |
| 957 | #endif |
| 958 | |
| 959 | #ifndef CURL_DISABLE_HTTP |
| 960 | static bool url_match_http_multiplex(struct connectdata *conn, |
| 564 | * |
| 565 | * Return a TRUE, iff the transfer can be done over an (appropriate) |
| 566 | * multiplexed connection. |
| 567 | */ |
| 568 | static bool xfer_may_multiplex(const struct Curl_easy *data, |
| 569 | const struct connectdata *conn) |
| 570 | { |
| 571 | #ifndef CURL_DISABLE_HTTP |
| 572 | /* If an HTTP protocol and multiplexing is enabled */ |
| 573 | if((conn->scheme->protocol & PROTO_FAMILY_HTTP) && |
| 574 | (!conn->bits.protoconnstart || !conn->bits.close)) { |
| 575 | |
| 576 | if(Curl_multiplex_wanted(data->multi) && |
| 577 | (data->state.http_neg.allowed & (CURL_HTTP_V2x | CURL_HTTP_V3x))) |
| 578 | /* allows HTTP/2 or newer */ |
| 579 | return TRUE; |
| 580 | } |
| 581 | #else |
| 582 | (void)data; |
| 583 | (void)conn; |
| 584 | #endif |
| 585 | return FALSE; |
| 586 | } |
| 587 | |
| 588 | #ifndef CURL_DISABLE_PROXY |
| 589 | static bool proxy_info_matches(const struct proxy_info *data, |
| C1: proxy_info_matches function definition starts | |
| 590 | const struct proxy_info *needle) |
| 591 | { |
| 592 | if((data->proxytype == needle->proxytype) && |
| C1: compares proxytype, port, and host.name only | |
| C4: proxy_info_matches lacks credential comparison | |
| 593 | (data->port == needle->port) && |
| C1: compares port | |
| 594 | curl_strequal(data->host.name, needle->host.name)) |
| C1: compares host.name via curl_strequal | |
| 595 | return TRUE; |
| C1: returns TRUE if those three match; no user/passwd comparison | |
| 596 | |
| 597 | return FALSE; |
| 598 | } |
| 599 | |
| 600 | static bool socks_proxy_info_matches(const struct proxy_info *data, |
| 601 | const struct proxy_info *needle) |
| 602 | { |
| 603 | if(!proxy_info_matches(data, needle)) |
| C4: socks_proxy_info_matches DOES compare user/passwd (contrast) | |
| 604 | return FALSE; |
| 605 | |
| 606 | /* the user information is case-sensitive |
| 607 | or at least it is not defined as case-insensitive |
| 608 | see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1 */ |
| 609 | |
| 610 | /* curl_strequal does a case insensitive comparison, |
| 611 | so do not use it here! */ |
| 612 | if(Curl_timestrcmp(data->user, needle->user) || |
| C4: explicit user comparison for SOCKS | |
| 613 | Curl_timestrcmp(data->passwd, needle->passwd)) |
| C4: explicit passwd comparison for SOCKS | |
| 614 | return FALSE; |
| 615 | return TRUE; |
| 616 | } |
| 617 | #endif |
| 618 | |
| 619 | /* A connection has to have been idle for less than 'conn_max_idle_ms' |
| 620 | (the success rate is too low after this), or created less than |