HexBrain

curl · Report #4129638

TLS session cache ignores CURLOPT_SSL_CTX_FUNCTION identity and resumes one client's session for another

N/Astatus
mediumseverity
5upvotes
20 Oct 2026disclosed

How it was found

Researcher: Maturi Jaswanth Sai Program: curl (HackerOne) Asset: github.com/curl/curl, lib/vtls/vtls_scache.c Affected: curl 8.21.0, 8.22.0 and 8.23.0 (tag curl-8_23_0, 2026-10-14), plus master eb2b201a (8.23.1-DEV, 2026-10-19) Weakness: CWE-295, wrong client identity on a resumed TLS session. Same class as CVE-2016-5419 and CVE-2017-7468

Before we went into report to be clear I have created separate file for reproduce steps to validate manually for the team so check that.

What happens

CURLOPT_SSL_CTX_FUNCTION lets an application configure the TLS context, including loading a client certificate. Two easy handles that pick different client certificates only through this callback, without setting CURLOPT_SSLCERT or CURLOPT_SSLKEY, end up with the same TLS session-cache peer key, because the key is built from struct ssl_filter_config fields and never looks at the callback or its userdata.

The second handle opens its own TCP connection and its callback runs and loads its own certificate, but curl offers the first handle's cached TLS session. The server accepts the resumption, never asks for the second certificate, and continues as the first identity. Both requests return the first client's data and no error is reported.

Root cause

cf_ssl_peer_key_add_mtls() (8.23.0, lib/vtls/vtls_scache.c:126) adds the client certificate material that comes from the normal options:

  if(ssl->clientcert && ssl->clientcert[0]) {
    result = cf_ssl_peer_key_add_path(buf, "CCERT", ssl->clientcert, is_local);
    ...
  if(ssl->key && ssl->key[0]) {
    result = cf_ssl_peer_key_add_path(buf, "KEY", ssl->key, is_local);
    ...
  if(ssl->key_blob) {
    result = cf_ssl_peer_key_add_hash(buf, "KEYBlob", ssl->key_blob);

ssl_peer_key_build() calls it at line 320. There is no entry for data->set.ssl_fsslctx or data->set.ssl_fsslctxp, and no code that disables the session cache when the callback is set.

The connection-reuse matcher (match_ssl_primary_config() in lib/vtls/vtls_config.c) is fine and is not what causes this: the two requests use different connections (ids 0 and 1). The confusion happens at the TLS session layer.

Reproduce

One file, one command:

./poc_sslctx.py

It runs the bug case, the failed-workaround case and the control by itself.

The script generates two client certificates (alice, bob) and a server certificate, builds its C client against the installed libcurl, starts an mTLS server, and runs three cases. Server connections, callbacks and responses are printed while they happen:

  1. default — bug: both connections show alice-client
  2. forbid-reuse — callback sets CURLOPT_FORBID_REUSE, still alice-client
  3. no-session-cache — CURLOPT_SSL_SESSIONID_CACHE=0, server shows bob-client

To use a libcurl built from the curl-8_23_0 tag instead of the system library (full commands in REPRODUCE.md):

CURL_CFLAGS="-I$PWD/curl/include" \
CURL_LIBS="$PWD/curl/build/lib/libcurl.a -lssl -lcrypto -lpsl \
-lbrotlidec -lbrotlicommon -lz -lzstd -lpthread -ldl" \
./poc_sslctx.py

Full walkthrough, including building libcurl from source: [REPRODUCE.md].

Evidence

Server log, default case (8.23.0):

CONN#1 from ('127.0.0.1', 42158): client_cert_CN=alice-client  first_line=b'GET / HTTP/1.1'
CONN#2 from ('127.0.0.1', 42164): client_cert_CN=alice-client  first_line=b'GET / HTTP/1.1'

Client output from the same run:

[callback] loading alice.crt into SSL_CTX
connection id: 0

=== request 2: bob's SSL_CTX_FUNCTION ===
[callback] loading bob.crt into SSL_CTX
connection id: 1

resp1: seen-cert=alice-client
resp2: seen-cert=alice-client

connections: 0 then 1 (separate connections)
identity the server actually saw: alice-client
RESULT: BUG (bob authenticated as alice)

Control case with the session cache disabled:

CONN#2 from ('127.0.0.1', 54416): client_cert_CN=bob-client
resp2: seen-cert=bob-client
RESULT: OK (bob authenticated as bob)

The default case reproduced on every run with 8.23.0 and master, and also reproduced with 8.21.0 and 8.22.0. The control always flipped to bob.

Screenshot of the full run, all three cases in one capture:

The documented workaround does not work

docs/libcurl/opts/CURLOPT_SSL_CTX_FUNCTION.md says:

The CURLOPT_SSL_CTX_FUNCTION callback allows the application to reach in and modify SSL details in the connection without libcurl itself knowing anything about it, which then subsequently can lead to libcurl unknowingly reusing SSL connections with different properties. To remedy this you may set CURLOPT_FORBID_REUSE from the callback function.

Setting CURLOPT_FORBID_REUSE inside both callbacks gives two separate connections but the same result: the second connection resumes the first session and the server still sees alice-client. The workaround covers connection reuse, not session resumption. Only CURLOPT_SSL_SESSIONID_CACHE=0 changes the outcome.

What I checked

  • match_ssl_primary_config() in lib/vtls/vtls_config.c is complete for ssl_primary_config; this is not a connection-reuse bug.
  • fsslctx has 20 references in lib/: setopt storage, the OpenSSL, wolfSSL and mbedTLS backends that invoke the callback, DoH propagation and the callback bookkeeping in api.c. grep -rn fsslctx lib/vtls/vtls_scache.c returns nothing, so the session cache cannot see the callback at all.
  • docs/libcurl/opts/CURLOPT_SSL_SESSIONID_CACHE.md says "nothing ever should get hurt by attempting to reuse SSL session-IDs". This is a counterexample.
  • Searched curl issues and PRs for SSL_CTX_FUNCTION session, session resumption client cert and fsslctx; no existing report matches.

Suggested fix

Only reuse a cached session when the callback identity matches. The pointer values are enough:

  • add ssl_fsslctx and ssl_fsslctxp to the peer key in ssl_peer_key_build(), or
  • do not cache or resume sessions for handles where ssl_fsslctx is set.

A test can use two callbacks that load different client certs and assert the second connection is authenticated as the second identity.

Impact

An application that uses CURLOPT_SSL_CTX_FUNCTION to select a client certificate per request against the same host, for example a gateway or proxy that authenticates to one backend as different tenants, can have one tenant's request authenticated as another tenant. The correct callback runs and returns success, the server accepts a valid resumption, and nothing reports an error.

Preconditions are specific: the client certificate must be set only through the callback, and both handles must share a multi handle so they share the session cache. In exchange the impact is a server-side identity mix-up, which is why I rate this Medium. The project has fixed this class twice before (CVE-2016-5419, CVE-2017-7468) for the normal certificate options.

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