✓ProveIt
Examples / Report #3973190 Saved analysis

CVE-2026-86411: FTP connection reuse inherits client-certificate

curl · 2026-08-2712.5s analysis · $0.021 in model calls

What the report claims

A lower-privileged FTP transfer can reuse a TLS-upgraded connection authenticated with another handle's client certificate, inheriting its privileged identity and bypassing authorization.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2026-86411 FTP connection reuse inherits client-certificate

Shown for comparison after analysis. The pipeline did not see this label.

ProveIt's finding

Code supports the claim

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 lib/url.c:887 only compares SSL config when PROTOPT_SSL or may_tls is true; for FTP with CURLUSESSL_NONE, both are false
  • Curl_ftp_conns_match() at lib/ftp.c:4488 permits reuse when new use_ssl (0) is not greater than pooled use_ssl (2), allowing CURLUSESSL_CONTROL -> CURLUSESSL_NONE direction
  • FTP declares PROTOPT_SSL_REUSE so url_match_ssl_use() at lib/url.c:723 allows reuse of an existing TLS-upgraded connection
  • No re-authentication or new TLS handshake occurs on reuse, so the server sees the original client-certificate identity
  • This is a cross-handle authentication bypass reachable by any caller sharing a connection pool (multi handle with multiple easy handles)
How this analysis ran 5 claims · 9 citations verified

Checked against curl master on 2026-08-27 (b73a4f00b2a6). Test evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.

1 · Grounding
grounded
no model · 27 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 2689 tokens · $0.0003
3 · Evidence
9 code snippets
real curl source at b73a4f00b2a6 · 5 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
15390 tokens · 7.5s · $0.0207
5 · Receipts
9 of 9 citations verified
every citation must point at code the judge was shown
Review a draft reply to the reporter

Edit and verify before sending.

The report correctly identifies a regression from commit 4ff212f8ed. The code at lib/url.c:887 skips SSL config comparison for FTP with CURLUSESSL_NONE because may_tls=false and FTP lacks PROTOPT_SSL. Meanwhile lib/ftp.c:4488 allows CURLUSESSL_CONTROL->CURLUSESSL_NONE reuse. A handle without a client certificate can inherit a pooled TLS connection authenticated with another handle's certificate. This is a valid cross-handle authentication bypass. Suggested fix: url_match_ssl_config should also gate on whether the pooled connection already has TLS (Curl_conn_is_ssl), not only on whether the new handle wants TLS.

Inspect all claims and source code 5 claims · 9 code excerpts

Report claims

C1flaw supported

FTP connection reuse permits a later handle using CURLUSESSL_NONE to inherit the client-certificate identity of a previous handle that used CURLUSESSL_CONTROL

Read the reasoning

The code path shows all three conditions from the report align: FTP declares PROTOPT_SSL_REUSE, url_match_ssl_config skips SSL config comparison when may_tls=false, and Curl_ftp_conns_match permits CURLUSESSL_CONTROL -> CURLUSESSL_NONE reuse.

C2location supported

The flaw occurs in lib/url.c where url_match_ssl_config() does not compare client-certificate settings when may_tls is false

Read the reasoning

The cited lines show the exact gate: client-certificate configuration is only compared when the protocol is always-SSL (PROTOPT_SSL) or the new handle may use TLS (may_tls). For FTP with CURLUSESSL_NONE, neither is true.

C3trigger supported

A second FTP transfer with CURLUSESSL_NONE reuses the pooled connection that was previously upgraded with STARTTLS and authenticated with a client certificate

Read the reasoning

The connection matching logic permits a CURLUSESSL_NONE handle to match a pooled connection that was previously upgraded with STARTTLS (use_ssl=2).

C4impact supported

The later transfer accesses resources authorized only for the first handle's client certificate, resulting in unauthorized operations

Read the reasoning

Because the pooled TLS connection is reused without a new handshake or FTP login, the server continues to authorize requests under the original client-certificate identity. The code shows no mechanism to force re-authentication when reuse occurs.

C5reproduction not verifiable

Steps 1-19 in the reproduction section demonstrate the vulnerability and its fix

Read the reasoning

Reproduction steps are not code claims; they describe a test procedure. The provided code does not contain test infrastructure to verify these steps.

Read the original report (excerpt)

Full report on HackerOne ↗

## Summary An `ftp://` transfer configured with `CURLUSESSL_NONE` can reuse a pooled FTP control connection that a previous easy handle upgraded with STARTTLS and authenticated using a client certificate. The second handle performs neither a new TLS handshake nor a new FTP login, so the server continues to authorize it under the first handle's certificate identity. This is a regression introduced in commit `4ff212f8ed328fb95261e11add8f4d2f0616895a` ("url: connection reuse fixes for starttls"). It is closely related to CVE-2016-5420 (client-certificate identity reuse across connections). ## Root cause Three things combine to allow this: 1. FTP declares `PROTOPT_SSL_REUSE` (`lib/protocol.c`), so `url_match_ssl_use()` (`lib/url.c:712-730`) accepts an existing TLS-upgraded connection for a later plain `ftp://` transfer. 2. For `CURLUSESSL_NONE`, connection matching sets `may_tls = false` and `require_tls = false` (`lib/url.c:1187-1188`). `url_match_ssl_config()` (`lib/url.c:883-895`) only calls `Curl_ssl_conn_config_match()` when `PROTOPT_SSL` is set or `may_tls` is true. Neither condition holds, so the client-certificate configuration is never compared. 3. Commit `4ff212f8ed` changed `Curl_ftp_conns_match()` (`lib/ftp.c:4472-4492`) from exact `use_ssl` equality (`!=`) to rejecting only when the new value is *greater* than the pooled connection's value (`>`). This permits the `CURLUSESSL_CONTROL` -> `CURLUSESSL_NONE` direction. ## Impact A handle configured with no client certificate silently inherits the TLS session and certificate identity of a previous handle that used the same connection pool, same FTP destination, and same FTP credentials. The second handle can then perform operations that the server authorizes based on the first handle's certificate. Prerequisites: 1. Two handles share a connection pool and target the same FTP server with the same `USER`/`PASS`. 2. The first handle uses `CURLUSESSL_CONTROL` with a privileged client certificate. 3. The second handle uses `CURLUSESSL_NONE` with no client certificate. 4. The FTP server derives authorization from the TLS client certificate. I consider this Low severity given the narrow prerequisites, but the result is a cross-handle authentication bypass, not merely stronger-than-requested encryption. ## Affected versions curl 8.21.0, plus affected 8.22.0 prerelease/development versions. Introduc …

Source code

lib/url.clines 687–755 · lines 712-730 cited by the report
687
688static bool url_match_multiplex_limits(struct connectdata *conn,
689 struct url_conn_match *m)
690{
691 if(CONN_INUSE(conn) && m->may_multiplex) {
692 DEBUGASSERT(conn->bits.multiplex);
693 /* If multiplexed, make sure we do not go over concurrency limit */
694 if(conn->attached_xfers >=
695 Curl_multi_max_concurrent_streams(m->data->multi)) {
696 infof(m->data, "client side MAX_CONCURRENT_STREAMS reached"
697 ", skip (%u)", conn->attached_xfers);
698 return FALSE;
699 }
700 if(conn->attached_xfers >=
701 Curl_conn_get_max_concurrent(m->data, conn, FIRSTSOCKET)) {
702 infof(m->data, "MAX_CONCURRENT_STREAMS reached, skip (%u)",
703 conn->attached_xfers);
704 return FALSE;
705 }
706 /* When not multiplexed, we have a match here! */
707 infof(m->data, "Multiplexed connection found");
708 }
709 return TRUE;
710}
711
712static bool url_match_ssl_use(struct connectdata *conn,
C1: url_match_ssl_use() allows reuse when protocol has PROTOPT_SSL_REUSE (FTP does) and connection is already SSL
713 struct url_conn_match *m)
714{
715 if(m->needle->scheme->flags & PROTOPT_SSL) {
716 /* We are looking for SSL, if `conn` does not do it, not a match. */
717 if(!Curl_conn_is_ssl(conn, FIRSTSOCKET))
718 return FALSE;
719 }
720 else if(Curl_conn_is_ssl(conn, FIRSTSOCKET)) {
C3: url_match_ssl_use allows reuse of an existing SSL connection when protocol has PROTOPT_SSL_REUSE
721 /* If the protocol does not allow reuse of SSL connections OR
722 is of another protocol family, not a match. */
723 if(!(m->needle->scheme->flags & PROTOPT_SSL_REUSE) ||
724 (get_protocol_family(conn->scheme) != m->needle->scheme->protocol))
725 return FALSE;
726 }
727 else if(m->require_tls)
728 /* a clear-text STARTTLS protocol with required TLS */
729 return FALSE;
730 return TRUE;
731}
732
733#ifndef CURL_DISABLE_PROXY
734static bool url_match_proxy_use(struct connectdata *conn,
735 struct url_conn_match *m)
736{
737 if(m->needle->bits.origin_is_proxy != conn->bits.origin_is_proxy)
738 return FALSE;
739
740 if(!proxy_info_matches(&m->needle->socks_proxy, &conn->socks_proxy))
741 return FALSE;
742
743 if(!proxy_info_matches(&m->needle->http_proxy, &conn->http_proxy))
744 return FALSE;
745
746 if(CURL_PROXY_IS_HTTPS(m->needle->http_proxy.proxytype)) {
747 /* https proxies come in different types, http/1.1, h2, ... */
748 /* match SSL config to proxy */
749 if(!Curl_ssl_conn_config_match(m->data, conn, TRUE)) {
750 DEBUGF(infof(m->data,
751 "Connection #%" FMT_OFF_T
752 " has different SSL proxy parameters, cannot reuse",
753 conn->connection_id));
754 return FALSE;
755 }
lib/url.clines 1162–1213 · lines 1187-1188 cited by the report
1162 bool success;
1163
1164 DEBUGASSERT(!data->conn);
1165
1166 memset(&match, 0, sizeof(match));
1167 match.data = data;
1168 match.needle = needle;
1169 match.now = *Curl_pgrs_now(data);
1170 match.may_multiplex = xfer_may_multiplex(data, needle);
1171
1172 Curl_cpool_prune_dead(cpool, data);
1173
1174#ifdef USE_NTLM
1175 match.want_ntlm_http =
1176 (data->state.authhost.want & CURLAUTH_NTLM) &&
1177 (needle->scheme->protocol & PROTO_FAMILY_HTTP);
1178#ifndef CURL_DISABLE_PROXY
1179 match.want_proxy_ntlm_http =
1180 needle->http_proxy.creds &&
1181 (data->state.authproxy.want & CURLAUTH_NTLM) &&
1182 (needle->scheme->protocol & PROTO_FAMILY_HTTP);
1183#endif
1184#endif
1185
1186#if !defined(CURL_DISABLE_HTTP) && defined(USE_SPNEGO)
1187 match.want_nego_http =
1188 (data->state.authhost.want & CURLAUTH_NEGOTIATE) &&
1189 (needle->scheme->protocol & PROTO_FAMILY_HTTP);
1190#ifndef CURL_DISABLE_PROXY
1191 match.want_proxy_nego_http =
1192 needle->http_proxy.creds &&
1193 (data->state.authproxy.want & CURLAUTH_NEGOTIATE) &&
1194 (needle->scheme->protocol & PROTO_FAMILY_HTTP);
1195#endif
1196#endif
1197 match.require_tls = data->set.use_ssl >= CURLUSESSL_CONTROL;
1198 match.may_tls = data->set.use_ssl > CURLUSESSL_NONE;
C1: For CURLUSESSL_NONE, may_tls = false (0 > 0 is false)
1199
1200 /* Find a connection in the pool that matches what "data + needle"
1201 * requires. If a suitable candidate is found, it is attached to "data". */
1202 success = Curl_cpool_find(data, needle->destination,
1203 url_match_conn, url_match_result, &match);
1204
1205 /* wait_pipe is TRUE if we encounter a bundle that is undecided. There
1206 * is no matching connection then, yet. */
1207 *waitpipe = (bool)match.wait_pipe;
1208 return success;
1209}
1210
1211/*
1212 * Allocate and initialize a new connectdata object.
1213 */
lib/url.clines 858–920 · lines 883-895 cited by the report
858
859 return TRUE;
860}
861
862static bool url_match_destination(struct connectdata *conn,
863 struct url_conn_match *m)
864{
865 /* Different connect-to peers never match */
866 if(!Curl_peer_same_destination(m->needle->via_peer, conn->via_peer))
867 return FALSE;
868
869 if(m->needle->origin->scheme != conn->origin->scheme) {
870 /* `needle` and `conn` not having the same scheme.
871 * This is allowed for the same family *if* conn is using TLS.
872 * - IMAP+STARTTLS works for IMAPS.
873 * - IMAPS works for IMAP. */
874 if(get_protocol_family(conn->origin->scheme) !=
875 m->needle->scheme->protocol) {
876 return FALSE;
877 }
878 }
879 /* Scheme mismatch is acceptable, compare hostname/port */
880 return Curl_peer_same_destination(m->needle->origin, conn->origin);
881}
882
883static bool url_match_ssl_config(struct connectdata *conn,
884 struct url_conn_match *m)
885{
886 /* If talking/upgrading to TLS, conn needs to use the same SSL options. */
887 if(((m->needle->scheme->flags & PROTOPT_SSL) || m->may_tls) &&
C1: url_match_ssl_config() only calls Curl_ssl_conn_config_match when PROTOPT_SSL or may_tls is true; neither holds for FTP with CURLUSESSL_NONE
C2: Condition ((m->needle->scheme->flags & PROTOPT_SSL) || m->may_tls) is false for FTP with CURLUSESSL_NONE, so Curl_ssl_conn_config_match is never called
C4: No SSL config comparison occurs, so client-certificate settings are not verified to match
888 !Curl_ssl_conn_config_match(m->data, conn, FALSE)) {
889 DEBUGF(infof(m->data, "Connection #%" FMT_OFF_T
890 " has different SSL parameters, cannot reuse",
891 conn->connection_id));
892 return FALSE;
893 }
894 return TRUE;
895}
896
897#if defined(USE_SPNEGO) || defined(USE_NTLM)
898static bool url_allow_sspi_empty_creds(struct Curl_creds *conn_creds,
899 struct Curl_easy *data,
900 struct connectdata *conn)
901{
902#ifdef USE_WINDOWS_SSPI
903 /* Empty user: SSPI on Windows can make use of an "ambient"
904 * user from a "SecurityToken" associated with the current thread or
905 * process. This token can be switched at any time. We are therefore
906 * not able to find out reliably what token the connection really
907 * used, nor what token in the next connect attempt will use.
908 * To avoid TOCTOU attacks, do not reuse on empty credentials
909 * UNLESS this connection is the one used by this transfer before. */
910 if(!Curl_creds_has_user(conn_creds) &&
911 (data->state.lastconnect_id != conn->connection_id))
912 return FALSE;
913#else
914 (void)conn_creds;
915 (void)data;
916 (void)conn;
917#endif
918 return TRUE;
919}
920#endif /* USE_SPNEGO || USE_NTLM */
lib/ftp.clines 4447–4517 · lines 4472-4492 cited by the report
4447 ftpc->alternative_to_user =
4448 curlx_strdup(CURL_EASY_STR(data, STRING_FTP_ALTERNATIVE_TO_USER));
4449 if(!ftpc->alternative_to_user) {
4450 curlx_safefree(ftpc->account);
4451 Curl_conn_meta_remove(conn, CURL_META_FTP_CONN);
4452 return CURLE_OUT_OF_MEMORY;
4453 }
4454 }
4455
4456 ftp->path = &data->state.up.path[1]; /* do not include the initial slash */
4457
4458 type_url_check(data, ftp);
4459
4460 /* get some initial data into the ftp struct */
4461 ftp->transfer = PPTRANSFER_BODY;
4462 ftp->downloadsize = 0;
4463 ftpc->known_filesize = -1; /* unknown size for now */
4464 ftpc->use_ssl = data->set.use_ssl;
4465 ftpc->ccc = data->set.ftp_ccc;
4466
4467 CURL_TRC_FTP(data, "[%s] setup connection -> %d", FTP_CSTATE(ftpc),
4468 (int)result);
4469 return result;
4470}
4471
4472bool Curl_ftp_conns_match(struct connectdata *needle, struct connectdata *conn)
4473{
4474 struct ftp_conn *nftpc = Curl_conn_meta_get(needle, CURL_META_FTP_CONN);
4475 struct ftp_conn *cftpc = Curl_conn_meta_get(conn, CURL_META_FTP_CONN);
4476 /* Also match ACCOUNT, ALTERNATIVE-TO-USER and CCC options */
4477 if(!nftpc || !cftpc ||
4478 Curl_timestrcmp(nftpc->account, cftpc->account) ||
4479 Curl_timestrcmp(nftpc->alternative_to_user,
4480 cftpc->alternative_to_user) ||
4481 (nftpc->ccc != cftpc->ccc))
4482 return FALSE;
4483 /* A mismatch on `use_ssl` MUST have been found in connection matching
4484 * before we come here. This is a check on MAYBE/MUST use of STARTTLS and
4485 * it only works on FTP. But IMAP/SMTP etc have the same `use_ssl` and
4486 * no extra match like FTP. We lack tests in this area, so let FTP fail
4487 * loudly here to help other cases. */
4488 if(nftpc->use_ssl > cftpc->use_ssl) {
C1: Curl_ftp_conns_match() only rejects when new use_ssl > pooled use_ssl; 0 > 2 is false, so CURLUSESSL_NONE matches a CURLUSESSL_CONTROL connection
C3: Curl_ftp_conns_match allows reuse when new use_ssl (0) is not greater than pooled use_ssl (2)
C4: Connection reuse proceeds without re-authentication
4489 DEBUGASSERT(0);
4490 return FALSE;
4491 }
4492 return TRUE;
4493}
4494
4495/*
4496 * FTP protocol.
4497 */
4498const struct Curl_protocol Curl_protocol_ftp = {
4499 ftp_setup_connection, /* setup_connection */
4500 ftp_do, /* do_it */
4501 ftp_done, /* done */
4502 ftp_do_more, /* do_more */
4503 ftp_connect, /* connect_it */
4504 ftp_multi_statemach, /* connecting */
4505 ftp_doing, /* doing */
4506 ftp_pollset, /* proto_pollset */
4507 ftp_pollset, /* doing_pollset */
4508 ftp_domore_pollset, /* domore_pollset */
4509 ZERO_NULL, /* perform_pollset */
4510 ftp_disconnect, /* disconnect */
4511 ZERO_NULL, /* write_resp */
4512 ZERO_NULL, /* write_resp_hd */
4513 ZERO_NULL, /* connection_is_dead */
4514 ZERO_NULL, /* attach connection */
4515 ZERO_NULL, /* follow */
4516};
4517
include/curl/curl.hlines 934–934 · definition of CURLUSESSL_CONTROL
Show 1 lines (not cited by the judge)
934#define CURLUSESSL_CONTROL 2L /* SSL for the control connection or fail */
include/curl/curl.hlines 932–932 · definition of CURLUSESSL_NONE
Show 1 lines (not cited by the judge)
932#define CURLUSESSL_NONE 0L /* do not attempt to use SSL */
lib/vtls/vtls_config.clines 171–184 · definition of Curl_ssl_conn_config_match
Show 14 lines (not cited by the judge)
171bool Curl_ssl_conn_config_match(struct Curl_easy *data,
172 struct connectdata *candidate,
173 bool proxy)
174{
175#ifndef CURL_DISABLE_PROXY
176 if(proxy)
177 return match_ssl_primary_config(data, &data->set.proxy_ssl.primary,
178 &candidate->proxy_ssl_config);
179#else
180 (void)proxy;
181#endif
182 return match_ssl_primary_config(data, &data->set.ssl.primary,
183 &candidate->ssl_config);
184}
lib/protocol.hlines 207–207 · definition of PROTOPT_SSL
Show 1 lines (not cited by the judge)
207#define PROTOPT_SSL (1 << 0) /* uses SSL */
lib/protocol.hlines 234–234 · definition of PROTOPT_SSL_REUSE
Show 1 lines (not cited by the judge)
234#define PROTOPT_SSL_REUSE (1 << 15) /* this protocol may reuse an existing