curl · Report #4122227
wolfSSL TLS sessions retain native-CA trust after the application narrows trust
How it was found
Summary
In curl 8.23.0 with the wolfSSL backend, a TLS session authenticated while the build-default native CA store is active can be resumed after the application changes the same easy handle to a policy that disables native trust. The second transfer succeeds and exchanges HTTP data even though an otherwise identical full handshake under the new policy rejects the peer.
The TLS session-cache key contains the raw CURLOPT_SSL_OPTIONS mask and CA
file/path/blob inputs, but not the resolved native_ca_store boolean. That
boolean can differ while every keyed field remains identical. wolfSSL then
applies the ineligible cached session and a resumed TLS 1.2 handshake does not
repeat certificate-chain authentication.
The minimal reproducer uses one ordinary easy handle and documented public APIs. It forces a new TCP connection for every request, so this is not the connection-reuse problem fixed as CVE-2026-11564. The issue remains in 8.21.0 and later releases that are documented as fixed for that CVE.
I suggest Low severity and CWE-295 (Improper Certificate Validation). The prerequisites are uncommon, but the outcome is a real trust-policy bypass with confidentiality and integrity impact.
Affected scope
Confirmed end to end with curl's wolfSSL backend:
- curl 8.19.0: affected
- curl 8.20.0: affected
- curl 8.21.0: affected, despite containing the CVE-2026-11564 fix
- curl 8.22.0: affected
- curl 8.23.0: affected
- current master
eaa9103225bff16bdf1bb2842f312897b1342e85: affected
The generic CURL_CA_NATIVE build option was introduced by
1730407b74f41cfd33f189348be54d0504b7c291, first present in curl 8.19.0.
The session cache capable of this collision predates it.
For the distinct post-CVE regression, the important affected range is 8.21.0 through 8.23.0/current. Versions 8.19.0 and 8.20.0 also exhibit the result but were already affected by the broader sticky-state behavior assigned CVE-2026-11564.
I confirmed the security outcome only with wolfSSL 5.9.4 built with
WOLFSSL_SYS_CA_CERTS. A current GnuTLS control tried the cached session but
still rejected the peer under the restricted policy. I therefore do not claim
GnuTLS as affected. Other backends should be evaluated separately.
Root cause
ssl_easy_config_compl_options() resolves the effective policy independently
of the raw SSL option mask. With a CURL_CA_NATIVE build, native trust becomes
the default only while no CA setting was explicitly selected:
primary->native_ca_store = !!(options & CURLSSLOPT_NATIVE_CA);
...
if(!sslc->custom_capath && !sslc->custom_cafile && !sslc->custom_cablob)
ssl_origin->native_ca_store = TRUE;
match_ssl_primary_config() compares native_ca_store, so curl already
recognizes this state as security-relevant when deciding whether a live
connection can be reused. ssl_peer_key_build() does not. It adds
ssl->ssl_options, CAfile, CApath, and CA blob content, but never adds
ssl->native_ca_store.
For the curl 8.23.0 minimal path built with CURL_CA_NATIVE=ON, no compiled CA
bundle, and no compiled CA path, the configurations are:
| State | custom CA selected | CA file | raw SSL options | resolved native CA |
|---|---|---|---|---|
| Warmup | no | NULL | 0 | true |
| Restricted | yes (CURLOPT_CAINFO=NULL) | NULL | 0 | false |
The peer keys are identical. Treating NULL as a real custom choice is
intentional in 8.23.0: commit
9a6d4ffc1b6c184fb2ec7c6e4879c835cb367689 restored that behavior, and the
CURLOPT_CAINFO documentation says NULL disables use of the option.
There is also a non-NULL collision that reproduces in every tested release
from 8.19.0 through 8.23.0. Build curl with both CURL_CA_NATIVE=ON and an
explicit CURL_CA_BUNDLE=/path/to/bundle.pem. The default transfer has native
trust plus that bundle. Setting CURLOPT_CAINFO to the same bundle path for
the second transfer disables native trust but leaves the keyed CA path
unchanged. This is the practical policy transition of retaining a shipped CA
bundle while excluding platform-added roots.
In wssl_apply_session(), curl deserializes the matching cached session and
calls wolfSSL_set_session() without checking whether the effective native-CA
policy matches the one that authenticated the session.
Relationship to CVE-2026-11564
CVE-2026-11564 covered an easy handle retaining default native trust after an
application switched to custom CA material. Its fix,
d69bfad3fa3daf5e72331f6870667607828d5891, recalculates
native_ca_store for each transfer and prevents an old live connection from
matching the new configuration.
That fix does not change lib/vtls/vtls_scache.c. The recalculated values are
correct, but the TLS-session cache maps the two different values to the same
key. The reproducer sets both CURLOPT_FRESH_CONNECT and
CURLOPT_FORBID_REUSE; curl 8.21.0 through 8.23.0 still resume the old TLS
session on a new TCP connection. This is therefore a separate incomplete-fix
path crossing the same security boundary in released versions advertised as
fixed.
Official prior advisory: https://curl.se/docs/CVE-2026-11564.html
Minimal reproducer
The attachment contains repro-null.c and run.sh. Requirements are curl
8.23.0 source, CMake, a C compiler, libidn2 development files, and wolfSSL
5.9.4 built with curl support and WOLFSSL_SYS_CA_CERTS.
Run:
./run.sh /path/to/curl-8.23.0 /path/to/wolfssl-prefix \
/tmp/curl-native-session-build https://example.com/
The URL can be replaced with any public HTTPS endpoint that is trusted by the machine's native store and accepts TLS 1.2 session resumption.
Expected vulnerable output:
control=77(Problem with the SSL CA cert (path? access rights?)) warmup=0 trigger=0 trigger_reused=1 trigger_body=577
The response size can vary. The program exits zero only if:
- a new handle with the restricted policy and its session cache disabled rejects the peer;
- the warmup succeeds under native trust;
- the same easy handle succeeds after the restrictive option change;
- curl emits its
SSL reusing sessiondiagnostic; and - response body bytes are received.
Every transfer forces fresh TCP and forbids retaining the connection. TLS 1.2 is forced to make ticket reuse deterministic. Peer and hostname verification remain enabled.
I obtained the same vulnerable output in three consecutive ASan+LSan runs on curl 8.23.0 with no sanitizer diagnostics.
Version and policy controls
The attachment also includes repro-same-easy.c, which uses a valid unrelated
CA bundle rather than NULL. With a build whose compiled bundle is that same
file, the exact result was stable on all five released tags:
control=77 warmup=0 trigger=0 trigger_reused=1
Tested commits:
- 8.19.0:
8c908d2d0a6d32abdedda2c52e90bd56ec76c24d - 8.20.0:
a05f34973e6c4bb629d018f7cb51487be1c904d8 - 8.21.0:
68720b4837284335b2d63cb358f8f6ce65f5bc55 - 8.22.0:
01346829096c61b372692f6dc43ffa778c6caccd - 8.23.0:
3163ec065643ba20709f190111f42c3e1fe95273
A separate key-separation control performs the warmup with an explicit
CURLSSLOPT_NATIVE_CA bit. That changes the raw option mask and therefore the
existing peer key. The restrictive request then fails and no session is
reused:
warmup=0 control=77 trigger=0 trigger_reused=1 separated_warmup=0 separated=77 separated_reused=0
This demonstrates that the success depends on the cache-key collision rather than connection reuse or a generally ineffective CA policy.
Patched control
fix.patch adds the resolved native-store state to the peer key when peer
verification is active and adds a unit3304 regression test. With that patch,
the same ASan build produces:
control=77(Problem with the SSL CA cert (path? access rights?)) warmup=0 trigger=77 trigger_reused=0 trigger_body=0
The new unit test passes. git diff --check is clean.
Suggested fix
Include the resolved effective state in ssl_peer_key_build(), for example:
if(ssl->verifypeer && ssl->native_ca_store) {
result = curlx_dyn_add(&buf, ":NATIVE-CA");
if(result)
goto out;
}
The effective boolean is the correct discriminator. Keying only on the raw
CURLSSLOPT_NATIVE_CA bit is insufficient because build defaults resolve the
same raw mask differently depending on whether custom CA inputs were chosen.
Duplicate review
I searched curl's public issues, pull requests, discussions, advisory list,
and the history of vtls_scache.c for native-CA/session combinations. I found
no report of this cache-key omission. PR #22235 concerns preserving successful
Apple SecTrust verification inside an OpenSSL session; it does not separate
sessions after the effective native-CA policy changes. The public fix for
CVE-2026-11564 likewise does not touch the session cache.
Relevant design documentation says different TLS configurations must produce
different ssl_peer_key values:
https://github.com/curl/curl/blob/master/docs/internals/TLS-SESSIONS.md
Suggested classification
- Severity: Low
- CWE: CWE-295, Improper Certificate Validation
- Component: libcurl TLS session cache / wolfSSL
- Impact: confidentiality and integrity after a documented CA trust-policy transition
Impact
An application can first make a non-sensitive request to an origin using the platform trust store, then deliberately narrow trust before a later sensitive request to the same origin. If the remote endpoint resumes the earlier TLS session, curl with wolfSSL continues the transfer under the old trust decision. The endpoint can receive credentials or request content and control the response even though a new handshake under the application's current policy would reject it.
This matters when custom CA configuration is used to exclude platform roots, including locally or administratively installed roots. An endpoint accepted under that broader native policy can retain access for the lifetime of its cached session after the application withdraws that trust.
The attacker must control the endpoint that established the original session, or otherwise possess its session-resumption state. A generic unrelated MitM cannot resume another server's ticket. The attack also requires the same origin, a NativeCA+wolfSSL build, and a server that accepts resumption. These constraints make Low appropriate, but do not remove the trust boundary. The same trust transition and impact were classified Low/CWE-295 in CVE-2026-11564.