curl · Report #3963725
wolfSSL backend disables hostname verification when CURLOPT_SSL_VERIFYPEER is 0
How it was found
Summary
When curl is built against wolfSSL, setting CURLOPT_SSL_VERIFYPEER=0 causes a hostname-mismatched certificate to be accepted even though CURLOPT_SSL_VERIFYHOST remains at its default enabled setting. I built wolfSSL from source, built curl against it, and connected to a TLS server presenting a certificate for an unrelated hostname (CN=totally-wrong-hostname.invalid) while requesting https://localhost/; the handshake completed and the HTTP request/response exchanged normally. This reproduced identically on released curl 8.21.0 and current HEAD (5c61e16869 / 8.22.0-DEV). A control run of the identical reproducer against curl's OpenSSL backend correctly rejected the same certificate ("certificate subject name ... does not match target hostname"), confirming this is specific to the wolfSSL backend rather than a general curl behavior. A second control -- the wolfSSL backend with fully default options (CURLOPT_SSL_VERIFYPEER left at its default of 1) -- correctly rejected the same server, isolating the bug precisely to the VERIFYPEER=0 condition.
Affected Version
- curl/libcurl 8.21.0 (release tag curl-8_21_0), built with -DCURL_USE_WOLFSSL=ON against wolfSSL built from upstream source
- Also reproduced on current HEAD, commit 5c61e16869 (8.22.0-DEV), identical build configuration
- wolfSSL version used: built from wolfSSL upstream master (~5.9.2) via ./configure --enable-opensslextra --enable-curl --enable-static --enable-shared && make && make install
- curl -V output from the HEAD build used: curl 8.22.0-DEV (Linux) libcurl/8.22.0-DEV wolfSSL/5.9.2 zlib/1.3.1
- Affected: curl/libcurl when built with the wolfSSL TLS backend. Not claiming this affects any other TLS backend; OpenSSL was tested directly as a control and does not exhibit this behavior (see below).
Steps to Reproduce
- Build wolfSSL from source with --enable-opensslextra --enable-curl.
- Build curl (release tag curl-8_21_0, or HEAD) with -DCURL_USE_WOLFSSL=ON against that wolfSSL build.
- Generate a self-signed certificate for a hostname that does not match the target, e.g. CN=totally-wrong-hostname.invalid.
- Start a minimal TLS server (Python ssl module is sufficient) on localhost using that certificate.
- Using a minimal libcurl client, set CURLOPT_URL to https://localhost:<port>/, set CURLOPT_SSL_VERIFYPEER to 0, and leave CURLOPT_SSL_VERIFYHOST unset (default enabled).
- Call curl_easy_perform().
- Observe the TLS handshake completes and the HTTP request/response exchange succeeds despite the hostname mismatch.
- Control 1: repeat the identical reproducer linked against curl's OpenSSL backend instead. Observe it correctly rejects the connection.
- Control 2: repeat the identical reproducer against the wolfSSL backend with fully default options (do not set CURLOPT_SSL_VERIFYPEER at all). Observe it correctly rejects the connection, isolating the bug to the VERIFYPEER=0 condition specifically.
This was implemented as a minimal libcurl C reproducer (tls_repro.c) driving a minimal Python TLS test server (https_server.py) that serves the mismatched-hostname certificate and logs whether the handshake completed. Both are attached to this report as the PoC.
Vulnerable Source
lib/vtls/wolfssl.c:
- wssl_ctx_init() / connection setup: sets wolfSSL_CTX_set_verify(wctx->ssl_ctx, conn_config->verifypeer ? WOLFSSL_VERIFY_PEER : WOLFSSL_VERIFY_NONE, NULL) -- so the whole wolfSSL-internal verification pipeline runs in non-failing (WOLFSSL_VERIFY_NONE) mode whenever verifypeer is off.
- wssl_connect_step1(): registers an SNI-based hostname check via wolfSSL_check_domain_name(wssl->ssl, connssl->peer.sni), gated only on conn_config->verifyhost (correctly independent of verifypeer at the point of registration).
- wssl_connect_step2(): after wolfSSL_connect(), there is an explicit, independent post-handshake re-check for the case where the target is a raw IP address (no SNI) -- but no equivalent independent re-check exists for the SNI/named-host case. The code goes straight to if(ret == WOLFSSL_SUCCESS) return CURLE_OK;. Under WOLFSSL_VERIFY_NONE, wolfSSL_connect() returns success regardless of whether the registered domain check matched, and for the SNI path there is nothing left afterward to catch that.
Net effect: the hostname check is registered independently of verifypeer, but its enforcement is not -- it is silently absorbed by the same "do not fail on verification problems" mode that verifypeer=0 turns on.
Weakness
Improper Validation of Certificate with Host Mismatch -- CWE-297. curl's own documentation states that CURLOPT_SSL_VERIFYHOST operates independently of CURLOPT_SSL_VERIFYPEER; on the wolfSSL backend that independence is not actually enforced at the point where it matters (post-handshake), even though it is correctly reflected at the point of registering the check.
Severity
Medium.
Justification: this is a demonstrated, reproducible hostname-verification bypass with real MITM impact, backed by curl's own historical precedent of assigning CVEs to this exact bug class (verifypeer/verifyhost independence failures) in a different backend context previously. We are not rating this higher than Medium initially because it requires a specific, non-default application configuration (CURLOPT_SSL_VERIFYPEER=0) combined with a specific TLS backend choice (wolfSSL) rather than being exploitable against curl's default configuration or an arbitrary curl build, and because we have not surveyed how common this backend/option combination is in real-world deployments.
Illustrative CVSS 3.1 vector for the demonstrated scenario: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N. Offered only as a concrete reference point for how we reasoned about the demonstrated test, not as a claim of the final score -- we recognize curl's security team makes the final severity determination.
Remediation
Add an explicit, independent post-handshake hostname check for the SNI (named-host) case in wssl_connect_step2(), structured like the existing IP-address-specific check that already exists a few lines below it: after a successful wolfSSL_connect(), when conn_config->verifyhost is set and an SNI hostname was used, verify the presented certificate's subject/SAN against that hostname independently of whatever WOLFSSL_VERIFY_NONE already decided, and fail the connection if it does not match -- mirroring how the IP-address path already re-checks via wolfSSL_X509_check_ip_asc() regardless of the configured verify mode.
Regression Test
Add a libcurl test case that, when built against wolfSSL, connects with CURLOPT_SSL_VERIFYPEER=0 and default CURLOPT_SSL_VERIFYHOST to a server presenting a certificate for an unrelated hostname, and asserts the connection is rejected with a peer/host verification error rather than succeeding.
Impact
Confirmed impact
An attacker positioned on the network path (or a compromised/malicious server) can present a certificate for any hostname and have it silently accepted by a curl/wolfSSL client that has set CURLOPT_SSL_VERIFYPEER=0 while relying on CURLOPT_SSL_VERIFYHOST's default to still protect against impersonation -- a common, documented, and reasonable pattern (e.g. connecting to internal hosts with self-signed or CA-less certificates while still expecting the requested hostname to be enforced). This defeats hostname-based MITM protection entirely for such connections. Demonstrated end to end with a real TLS handshake and HTTP exchange, not inferred from source reading alone.
Limitations
- Requires the application to explicitly set CURLOPT_SSL_VERIFYPEER=0; this is not curl's default behavior and is not triggerable without that specific application configuration.
- Requires curl to be built with the wolfSSL TLS backend specifically. We tested OpenSSL directly as a control and it is not affected. We have not tested every other curl TLS backend and are not claiming this is universal across backends.
- We have not identified a specific named open-source application that uses this exact option combination with the wolfSSL backend.