Overview
| Field | Value |
|---|---|
| CVE ID | CVE-2026-8932 |
| Component | libcurl (the curl CLI itself is not affected) |
| CWE | CWE-305 (Authentication Bypass by Primary Weakness) |
| Severity | Low (no published CVSS 3.1 from cve.org/Red Hat; curl's own rating is Low) |
| Affected versions | 7.7 through 8.20.0 |
| Fixed in | 8.21.0 (released 2026-06-24) |
| Reporter | Joshua Rogers (Aisle Research) |
To save time, libcurl keeps TLS connections around in a connection pool and reuses them for later transfers that look like they use the same settings. This vulnerability sits in the logic that decides "are these settings the same?" — a handful of client-certificate options were silently excluded from that comparison. As a result, two easy handles that only differed in SSLKEY, SSLKEYTYPE, KEYPASSWD, SSLCERTTYPE, or SSLKEYBLOB could end up sharing the same already-authenticated connection.
Root Cause — A Struct Split in Two
libcurl keeps TLS settings in two different structs:
-
ssl_primary_config— the struct actually used bymatch_ssl_primary_config()to decide whether a connection can be reused, and by the TLS session-cache key -
ssl_config_data— a larger struct holding the rest of the TLS configuration (it embedsssl_primary_configas a member)
The problem: the five fields needed to actually open a client certificate — cert_type, key, key_type, key_passwd, and key_blob — lived in ssl_config_data, not ssl_primary_config. Since match_ssl_primary_config() only compares ssl_primary_config, those five fields were entirely outside its field of view. Before the patch, ssl_config_data looked roughly like this:
struct ssl_config_data {
/* ... */
char *cert_type; /* format for certificate (default: PEM) */
char *key; /* private key filename */
struct curl_blob *key_blob;
char *key_type; /* format for private key (default: PEM) */
char *key_passwd; /* plain text private key password */
BIT(certinfo);
/* ... */
};
And the actual reuse-check function (lib/vtls/vtls.c) only compared ssl_primary_config fields:
static bool match_ssl_primary_config(struct Curl_easy *data,
struct ssl_primary_config *c1,
struct ssl_primary_config *c2)
{
if(Curl_safecmp(c1->CApath, c2->CApath) &&
Curl_safecmp(c1->CAfile, c2->CAfile) &&
Curl_safecmp(c1->issuercert, c2->issuercert) &&
Curl_safecmp(c1->clientcert, c2->clientcert) &&
/* ... curves, signature_algorithms, pinned_key, etc. ... */
Curl_safecmp(c1->CRLfile, c2->CRLfile) &&
Curl_safecmp(c1->pinned_key, c2->pinned_key))
return TRUE;
return FALSE;
}
clientcert (the certificate file path) was compared, but the fields that determine which private key opens that certificate — key and key_passwd — were not. So if two handles used the same SSLCERT but different SSLKEY values, the function would still conclude "same config" and let the connection be reused.
The diagram above shows this split: the left card lists the fields match_ssl_primary_config() actually compares (ssl_primary_config), and the right card lists the five fields that were excluded before the patch (ssl_config_data).
The TLS session cache had a parallel issue. Its cache key appended a fixed string, ":CCERT", to represent "this session used a client cert" — without encoding which certificate. So sessions using different client certificates could also collide in the session cache.
Attack Scenario
This becomes a real problem in applications where multiple easy handles share a connection pool — via CURLSH (a share handle) or a multi handle. A typical case is a server-side proxy or API gateway that authenticates different users to a backend over mTLS, each with their own client-certificate private key.
- Handle A connects with
SSLCERT=client.pem,SSLKEY=keyA.pem, completes the mTLS handshake, and the resulting connection is cached in the pool as "authenticated as user A". - Handle B sends a request using the same
SSLCERT=client.pembut a differentSSLKEY=keyB.pem. -
match_ssl_primary_config()only checks thatclientcertmatches, so Handle B is handed the already-authenticated connection from Handle A without a new TLS handshake. - From the server's point of view, this connection is still verified against user A's mTLS certificate, so it processes Handle B's request as if it came from user A.
The flow above walks through this step by step. In effect, two requests intended to authenticate as different identities end up sharing the same authenticated channel simply because their private keys differed — which is why this was classified as CWE-305 (Authentication Bypass). The curl team was explicit that this is a pure logic flaw, not a memory-safety bug, and pointed to CVE-2022-27782 as a similar prior case.
The Patch
The fix in commit 7541ae5 promotes the five affected fields from ssl_config_data into ssl_primary_config:
struct ssl_primary_config {
/* ... existing fields ... */
char *pinned_key;
char *CRLfile;
char *cert_type; /* moved here */
char *key; /* moved here */
char *key_type; /* moved here */
char *key_passwd; /* moved here */
struct curl_blob *cert_blob;
struct curl_blob *ca_info_blob;
struct curl_blob *issuercert_blob;
struct curl_blob *key_blob; /* moved here */
/* ... */
};
The comparison function now checks all five. Notably, key_passwd is compared with Curl_timestrcmp(), a timing-attack-resistant string comparison:
static bool match_ssl_primary_config(struct Curl_easy *data,
struct ssl_primary_config *c1,
struct ssl_primary_config *c2)
{
if(/* existing comparisons */
Curl_safecmp(c1->pinned_key, c2->pinned_key) &&
curl_strequal(c1->cert_type, c2->cert_type) &&
Curl_safecmp(c1->key, c2->key) &&
curl_strequal(c1->key_type, c2->key_type) &&
!Curl_timestrcmp(c1->key_passwd, c2->key_passwd))
return TRUE;
return FALSE;
}
Because the fields moved, every backend file referencing them had to change — 19 files in total, including openssl.c, gtls.c, mbedtls.c, rustls.c, schannel.c, wolfssl.c, libssh.c, libssh2.c, and ldap.c. References like ssl_config->key became ssl_config->primary.key. The OpenSSL backend, for example, changed like this:
/* before */
result = client_cert(data, octx->ssl_ctx, ssl_cert, ssl_cert_blob, ssl_cert_type,
ssl_config->key, ssl_config->key_blob,
ssl_config->key_type, ssl_config->key_passwd);
/* after */
result = client_cert(data, octx->ssl_ctx, ssl_cert, ssl_cert_blob, ssl_cert_type,
ssl_config->primary.key, ssl_config->primary.key_blob,
ssl_config->primary.key_type,
ssl_config->primary.key_passwd);
On the session-cache side, the fixed ":CCERT" marker was replaced with the actual clientcert path value, so sessions using different client certificates can no longer share a cache entry. clone_ssl_primary_config() and free_primary_ssl_config() also gained clone/free logic (CLONE_STRING, CLONE_BLOB, curlx_safefree) for the newly relocated fields. The fix was validated with two new test cases, 3303 and 3304.
Impact and Remediation
This bug traces back to commit a1d6ad2 around curl 7.7 (roughly 2010) and existed for nearly 16 years before being fixed in curl 8.21.0. The curl CLI itself is unaffected, since each invocation is a fresh process with its own connection pool. The exposure is limited to long-running applications that share a connection pool across multiple easy handles — via CURLSH or a multi handle — while switching client-certificate credentials between them, such as a proxy or gateway that authenticates different users over mTLS.
Remediation options:
- Upgrade libcurl to 8.21.0 or later (recommended)
- If upgrading isn't possible yet, backport commit
7541ae5and rebuild - As a temporary mitigation, avoid reusing handles when client-certificate credentials change — create a new handle instead



Top comments (0)