CVE-2026-8286: wrong STARTTLS connection reuse
What the report claims
A flaw in curl's STARTTLS connection reuse allows an attacker to reuse an unverified TLS session from a previous connection, bypassing verification and pinning checks.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2026-8286 wrong STARTTLS connection reuse
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?
- url_match_ssl_config at line 1028 only calls Curl_ssl_conn_config_match when needle->scheme->flags & PROTOPT_SSL
- url_match_ssl_use at lines 833-838 permits reuse of an SSL-upgraded connection for schemes with PROTOPT_SSL_REUSE (STARTTLS protocols) without requiring PROTOPT_SSL
- This creates a gap where STARTTLS connections are reused without comparing SSL configuration (verifypeer, pinned keys, etc.)
- The flaw is the same class as CVE-2022-27782 but affects the STARTTLS path that the previous fix missed
- Master at revision 455bebc2c7 still contains this logic gap
How this analysis ran 5 claims · 7 citations verified
Checked against curl permalink 455bebc2c7 (455bebc2c762). Dev 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 analysis. The code confirms the flaw: url_match_ssl_config (lib/url.c:1028) only invokes Curl_ssl_conn_config_match for PROTOPT_SSL schemes, while url_match_ssl_use (lib/url.c:833-838) allows STARTTLS schemes (with PROTOPT_SSL_REUSE) to reuse SSL-upgraded connections without that config check. This lets a connection with VERIFYPEER=0 be reused by one requiring VERIFYPEER=1 and pinning. We'll prepare a fix to extend the config comparison to the STARTTLS reuse path.
Inspect all claims and source code 5 claims · 4 code excerpts
Report claims
url_match_ssl_use() allows reuse of an SSL-upgraded connection via PROTOPT_SSL_REUSE without calling Curl_ssl_conn_config_match for imap://, pop3://, smtp://, ftp:// schemes with CURLOPT_USE_SSL=CURLUSESSL_ALL
Read the reasoning
For STARTTLS schemes (imap://, pop3://, smtp://, ftp://), the scheme lacks PROTOPT_SSL but has PROTOPT_SSL_REUSE. When an SSL-upgraded connection exists, url_match_ssl_use returns TRUE at line 838 (if protocol family matches), then url_match_ssl_config returns TRUE at line 1028 without calling Curl_ssl_conn_config_match because the PROTOPT_SSL gate fails.
url_match_ssl_use() at L833-838
Read the reasoning
The function url_match_ssl_use at lines 825-844 contains the cited logic at lines 833-838.
imap:// scheme with CURLOPT_USE_SSL=CURLUSESSL_ALL and CURLOPT_SSL_VERIFYPEER=0
Read the reasoning
The provided code confirms CURLUSESSL_ALL exists, but scheme flags (PROTOPT_SSL vs PROTOPT_SSL_REUSE for imap://) are not shown in the provided snippets. The trigger is plausible given the logic but not directly verifiable from provided code alone.
An on-path attacker can MITM the STARTTLS upgrade and reuse the unverified TLS session from a previous connection
Read the reasoning
Since Curl_ssl_conn_config_match is not called for STARTTLS schemes, SSL parameters like CURLOPT_SSL_VERIFYPEER and pinned keys are not compared during connection reuse, allowing a weak session to be reused by a connection requiring strong verification.
Connect handle A to imap://host with USE_SSL=CURLUSESSL_ALL and VERIFYPEER=0; then connect handle B to the same host with USE_SSL=CURLUSESSL_ALL and VERIFYPEER=1; B reuses A's unverified TLS session
Read the reasoning
The reproduction steps follow directly from the code path: handle A creates an SSL-upgraded connection with weak settings; handle B with strong settings matches the same connection via PROTOPT_SSL_REUSE and skips config comparison.
What the report says about other projects
not addressed The fix for CVE-2022-27782 added Curl_ssl_conn_config_match but only behind a PROTOPT_SSL gate, missing the STARTTLS path.
Read the original report
Source code
| 999 | if(m->needle->scheme != conn->scheme) { |
| 1000 | /* `needle` and `conn` do not have the same scheme... */ |
| 1001 | if(get_protocol_family(conn->scheme) != m->needle->scheme->protocol) { |
| 1002 | /* and `conn`s protocol family is not the protocol `needle` wants. |
| 1003 | * IMAPS would work for IMAP, but no vice versa. */ |
| 1004 | return FALSE; |
| 1005 | } |
| 1006 | /* We are in an IMAPS vs IMAP like case. We expect `conn` to have SSL */ |
| 1007 | if(!Curl_conn_is_ssl(conn, FIRSTSOCKET)) { |
| 1008 | DEBUGF(infof(m->data, "Connection #%" FMT_OFF_T |
| 1009 | " has compatible protocol family, but no SSL, no match", |
| 1010 | conn->connection_id)); |
| 1011 | return FALSE; |
| 1012 | } |
| 1013 | } |
| 1014 | |
| 1015 | /* `needle` must have the same hostname and port in origin and |
| 1016 | * via_peer (if present, NULL peers are equal) */ |
| 1017 | if(!Curl_peer_same_destination(m->needle->origin, conn->origin) || |
| 1018 | !Curl_peer_same_destination(m->needle->via_peer, conn->via_peer)) |
| 1019 | return FALSE; |
| 1020 | } |
| 1021 | return TRUE; |
| 1022 | } |
| 1023 | |
| 1024 | static bool url_match_ssl_config(struct connectdata *conn, |
| 1025 | struct url_conn_match *m) |
| 1026 | { |
| 1027 | /* If talking TLS, conn needs to use the same SSL options. */ |
| 1028 | if((m->needle->scheme->flags & PROTOPT_SSL) && |
| C1: url_match_ssl_config only calls Curl_ssl_conn_config_match when needle->scheme->flags & PROTOPT_SSL | |
| C4: SSL config match skipped for non-PROTOPT_SSL schemes | |
| C5: Config match skipped | |
| 1029 | !Curl_ssl_conn_config_match(m->data, conn, FALSE)) { |
| 1030 | DEBUGF(infof(m->data, "Connection #%" FMT_OFF_T |
| 1031 | " has different SSL parameters, cannot reuse", |
| 1032 | conn->connection_id)); |
| 1033 | return FALSE; |
| 1034 | } |
| 1035 | return TRUE; |
| 1036 | } |
| 1037 | |
| 1038 | #ifdef USE_NTLM |
| 1039 | static bool url_match_auth_ntlm(struct connectdata *conn, |
| 1040 | struct url_conn_match *m) |
| 1041 | { |
| 1042 | /* If we are looking for an HTTP+NTLM connection, check if this is |
| 1043 | already authenticating with the right credentials. If not, keep |
| 1044 | looking so that we can reuse NTLM connections if |
| 1045 | possible. (Especially we must not reuse the same connection if |
| 1046 | partway through a handshake!) */ |
| 1047 | if(m->want_ntlm_http) { |
| 1048 | if(Curl_timestrcmp(m->needle->user, conn->user) || |
| 1049 | Curl_timestrcmp(m->needle->passwd, conn->passwd)) { |
| 1050 | /* we prefer a credential match, but this is at least a connection |
| 1051 | that can be reused and "upgraded" to NTLM if it does |
| 1052 | not have any auth ongoing. */ |
| 1053 | #ifdef USE_SPNEGO |
| 1054 | if((conn->http_ntlm_state == NTLMSTATE_NONE) |
| 1055 | && (conn->http_negotiate_state == GSS_AUTHNONE)) { |
| 1056 | #else |
| 1057 | if(conn->http_ntlm_state == NTLMSTATE_NONE) { |
| 1058 | #endif |
| 1059 | m->found = conn; |
| 1060 | } |
| 1061 | return FALSE; |
| 800 | |
| 801 | static bool url_match_multiplex_limits(struct connectdata *conn, |
| 802 | struct url_conn_match *m) |
| 803 | { |
| 804 | if(CONN_INUSE(conn) && m->may_multiplex) { |
| 805 | DEBUGASSERT(conn->bits.multiplex); |
| 806 | /* If multiplexed, make sure we do not go over concurrency limit */ |
| 807 | if(conn->attached_xfers >= |
| 808 | Curl_multi_max_concurrent_streams(m->data->multi)) { |
| 809 | infof(m->data, "client side MAX_CONCURRENT_STREAMS reached" |
| 810 | ", skip (%u)", conn->attached_xfers); |
| 811 | return FALSE; |
| 812 | } |
| 813 | if(conn->attached_xfers >= |
| 814 | Curl_conn_get_max_concurrent(m->data, conn, FIRSTSOCKET)) { |
| 815 | infof(m->data, "MAX_CONCURRENT_STREAMS reached, skip (%u)", |
| 816 | conn->attached_xfers); |
| 817 | return FALSE; |
| 818 | } |
| 819 | /* When not multiplexed, we have a match here! */ |
| 820 | infof(m->data, "Multiplexed connection found"); |
| 821 | } |
| 822 | return TRUE; |
| 823 | } |
| 824 | |
| 825 | static bool url_match_ssl_use(struct connectdata *conn, |
| 826 | struct url_conn_match *m) |
| 827 | { |
| 828 | if(m->needle->scheme->flags & PROTOPT_SSL) { |
| 829 | /* We are looking for SSL, if `conn` does not do it, not a match. */ |
| 830 | if(!Curl_conn_is_ssl(conn, FIRSTSOCKET)) |
| 831 | return FALSE; |
| 832 | } |
| 833 | else if(Curl_conn_is_ssl(conn, FIRSTSOCKET)) { |
| C1: url_match_ssl_use allows reuse of SSL connection when needle lacks PROTOPT_SSL but has PROTOPT_SSL_REUSE and same protocol family | |
| C2: url_match_ssl_use logic at lines 833-838 matches report | |
| C5: Reuse path for SSL-upgraded connection with PROTOPT_SSL_REUSE | |
| 834 | /* If the protocol does not allow reuse of SSL connections OR |
| 835 | is of another protocol family, not a match. */ |
| 836 | if(!(m->needle->scheme->flags & PROTOPT_SSL_REUSE) || |
| 837 | (get_protocol_family(conn->scheme) != m->needle->scheme->protocol)) |
| 838 | return FALSE; |
| 839 | } |
| 840 | else if(m->req_tls) |
| 841 | /* a clear-text STARTTLS protocol with required TLS */ |
| 842 | return FALSE; |
| 843 | return TRUE; |
| 844 | } |
| 845 | |
| 846 | #ifndef CURL_DISABLE_PROXY |
| 847 | static bool url_match_proxy_use(struct connectdata *conn, |
| 848 | struct url_conn_match *m) |
| 849 | { |
| 850 | if(m->needle->bits.httpproxy != conn->bits.httpproxy || |
| 851 | m->needle->bits.socksproxy != conn->bits.socksproxy) |
| 852 | return FALSE; |
| 853 | |
| 854 | if(m->needle->bits.socksproxy && |
| 855 | !proxy_info_matches(&m->needle->socks_proxy, &conn->socks_proxy)) |
| 856 | return FALSE; |
| 857 | |
| 858 | if(m->needle->bits.httpproxy) { |
| 859 | if(m->needle->bits.tunnel_proxy != conn->bits.tunnel_proxy) |
| 860 | return FALSE; |
| 861 | |
| 862 | if(!proxy_info_matches(&m->needle->http_proxy, &conn->http_proxy)) |
| 863 | return FALSE; |
| 864 | |
| 865 | if(IS_HTTPS_PROXY(m->needle->http_proxy.proxytype)) { |
| 866 | /* https proxies come in different types, http/1.1, h2, ... */ |
| 867 | if(m->needle->http_proxy.proxytype != conn->http_proxy.proxytype) |
| 868 | return FALSE; |
| 869 | /* match SSL config to proxy */ |
| 227 | bool Curl_ssl_conn_config_match(struct Curl_easy *data, |
| C4: Curl_ssl_conn_config_match compares ssl_primary_config (verifypeer, pinned keys, etc.) | |
| 228 | struct connectdata *candidate, |
| 229 | bool proxy) |
| 230 | { |
| 231 | #ifndef CURL_DISABLE_PROXY |
| 232 | if(proxy) |
| 233 | return match_ssl_primary_config(data, &data->set.proxy_ssl.primary, |
| 234 | &candidate->proxy_ssl_config); |
| 235 | #else |
| 236 | (void)proxy; |
| 237 | #endif |
| 238 | return match_ssl_primary_config(data, &data->set.ssl.primary, |
| 239 | &candidate->ssl_config); |
| 240 | } |
Show 1 lines (not cited by the judge)
| 925 | #define CURLUSESSL_ALL 3L /* SSL for all communication or fail */ |