HexBrain

curl · Report #3826199

mbedTLS / wolfSSL / rustls backends silently skip hostname verification when CURLOPT_SSL_VERIFYPEER=0

N/Astatus
mediumseverity
18upvotes
26 Jun 2026disclosed

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 = 0 zeroes the entire mbedTLS verify-flag bitmap, including MBEDTLS_X509_BADCERT_CN_MISMATCH. mbedTLS uses MBEDTLS_SSL_VERIFY_REQUIRED and 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_NONE makes wolfSSL skip the entire certificate-validation pipeline including the hostname check registered by wolfSSL_check_domain_name further 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 returns RUSTLS_RESULT_OK unconditionally. It replaces rustls's default ServerCertVerifier, which in rustls performs both chain validation and hostname matching against the SNI value passed to rustls_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.3
  • build_and_run.sh — wrapper: docker build then docker run
  • run.sh — entrypoint inside container; drives all four scenarios
  • poc.c — minimal libcurl PoC
  • poc_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.

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