curl · Report #3826199
mbedTLS / wolfSSL / rustls backends silently skip hostname verification when CURLOPT_SSL_VERIFYPEER=0
How it was found
Summary
When an application sets CURLOPT_SSL_VERIFYPEER=0 while keeping
CURLOPT_SSL_VERIFYHOST=2 (the default), the mbedTLS, wolfSSL, and rustls
TLS backends silently skip the hostname-vs-certificate check. The OpenSSL,
GnuTLS, and Schannel backends correctly preserve hostname checking under
the same configuration.
docs/libcurl/opts/CURLOPT_SSL_VERIFYPEER.md lines 57-59 explicitly state:
"The check that the host name in the certificate is valid for the hostname you are connecting to is done independently of the CURLOPT_SSL_VERIFYPEER(3) option."
Result: an MITM holding any valid certificate (the CN does not need to
match the requested host) can intercept TLS sessions to any HTTPS target
when an application disables peer chain verification — a common pattern
with self-signed / internal-CA deployments. Authorization, Cookie, and
POST bodies all leak to the attacker.
Affected backends
$ src/curl --version # all four built from the same source tree
curl 8.21.1-DEV (x86_64-pc-linux-gnu) libcurl/8.21.1-DEV OpenSSL/3.5.6 ← rc=60 (correct)
curl 8.21.1-DEV (x86_64-pc-linux-gnu) libcurl/8.21.1-DEV mbedTLS/3.6.5 ← rc=0 (BUG)
curl 8.21.1-DEV (x86_64-pc-linux-gnu) libcurl/8.21.1-DEV wolfSSL/5.7.2 ← rc=0 (BUG)
curl 8.21.1-DEV (x86_64-pc-linux-gnu) libcurl/8.21.1-DEV rustls-ffi/0.15.3 ← rc=0 (BUG)
Affected versions: all curl releases through HEAD master for the
mbedTLS, wolfSSL, and rustls backends. The mbedTLS code path traces to
commit 88cae145509 (2024-08-17, "mbedtls: add more informative logging"),
shipped in 8.10.0 and unchanged through current master.
The relevant code sites:
-
mbedTLS —
lib/vtls/mbedtls.c::mbed_verify_cb:if(!conn_config->verifypeer) *flags = 0; /* clears CN_MISMATCH along with chain flags */ else if(!conn_config->verifyhost) *flags &= ~MBEDTLS_X509_BADCERT_CN_MISMATCH;*flags = 0zeroes the entire mbedTLS verify-flag bitmap, includingMBEDTLS_X509_BADCERT_CN_MISMATCH. mbedTLS usesMBEDTLS_SSL_VERIFY_REQUIREDand trusts whatever flags this callback returns. -
wolfSSL —
lib/vtls/wolfssl.c::Curl_wssl_ctx_init:wolfSSL_CTX_set_verify(wctx->ssl_ctx, conn_config->verifypeer ? WOLFSSL_VERIFY_PEER : WOLFSSL_VERIFY_NONE, NULL);When
verifypeer=0,WOLFSSL_VERIFY_NONEmakes wolfSSL skip the entire certificate-validation pipeline including the hostname check registered bywolfSSL_check_domain_namefurther down. -
rustls —
lib/vtls/rustls.c::cr_init_backend(line 1048-1050):if(!conn_config->verifypeer) { rustls_client_config_builder_dangerous_set_certificate_verifier( config_builder, cr_verify_none); }cr_verify_none(line 389) is a callback that returnsRUSTLS_RESULT_OKunconditionally. It replaces rustls's defaultServerCertVerifier, which in rustls performs both chain validation and hostname matching against the SNI value passed torustls_client_connection_new. Replacing it disables both at once.
Steps to reproduce
A self-contained Docker reproduction package is attached
(verifyhost-repro.tar.gz). It pulls the latest curl source from
github.com/curl/curl.git, builds four libcurls from the same source tree
(OpenSSL, mbedTLS, wolfSSL, rustls backends), runs a TLS server presenting
a wrong-CN certificate, and drives a libcurl client at all four backends.
tar xzf verifyhost-repro.tar.gz
cd docker
./build_and_run.sh
(Pass a tag/branch/SHA as arg to pin: ./build_and_run.sh curl-8_21_0.)
Expected output:
================================================================
curl source commit: <SHA from official curl/curl repo>
OpenSSL curl --version: curl 8.21.1-DEV libcurl/8.21.1-DEV OpenSSL/3.5.6 ...
mbedTLS curl --version: curl 8.21.1-DEV libcurl/8.21.1-DEV mbedTLS/3.6.5 ...
wolfSSL curl --version: curl 8.21.1-DEV libcurl/8.21.1-DEV wolfSSL/5.7.2 ...
rustls curl --version: curl 8.21.1-DEV libcurl/8.21.1-DEV rustls-ffi/0.15.3 ...
================================================================
============== OpenSSL backend (control) — expect rc=60 ==============
* SSL: certificate subject name 'wrong.example.com' does not match target hostname 'target.example.com'
=== RESULT: rc=60 (SSL peer certificate or SSH remote key was not OK) ===
============== mbedTLS backend — expect rc=0 (BUG) ==============
< HTTP/1.1 200 OK
< Content-Length: 7
=== RESULT: rc=0 (No error) ===
============== wolfSSL backend — expect rc=0 (BUG) ==============
< HTTP/1.1 200 OK
=== RESULT: rc=0 (No error) ===
============== rustls backend — expect rc=0 (BUG) ==============
< HTTP/1.1 200 OK
=== RESULT: rc=0 (No error) ===
The OpenSSL run is the control: same source tree, same flags, same PoC
code — only the TLS backend differs. Its rc=60 confirms that the PoC
correctly enforces verifyhost=2 and that the rc=0 on the other three
backends is a backend defect, not a PoC mistake.
Files in the package
Dockerfile— Debian trixie + mbedTLS 3.6.5 + OpenSSL 3.x + wolfSSL 5.7.2 + rustls-ffi 0.15.3build_and_run.sh— wrapper:docker buildthendocker runrun.sh— entrypoint inside container; drives all four scenariospoc.c— minimal libcurl PoCpoc_server.py— Python TLS server presenting CN=wrong.example.com
████████
████████
Impact
Summary:
An MITM on the network path can fully impersonate the intended HTTPS server using any certificate (no need to match the requested hostname), decrypting and modifying the entire TLS session. The application has opted out of chain verification but explicitly kept hostname verification on per the documented contract; libcurl silently disables both.