curl · Report #4128428
CVE-2026-115528: wolfSSL TLS connect without cert
How it was found
Summary:
With curl built against wolfSSL, a normal curl -u user:pass https://host/ with the default CURLOPT_SSL_VERIFYPEER=1 and CURLOPT_SSL_VERIFYHOST=2 completes a TLS 1.2 handshake with a server that sends no certificate at all (anonymous DH suite), reports ssl_verify_result=0, and sends the credentials to that server. OpenSSL and GnuTLS builds refuse the same server.
The docs promise otherwise.
CURLOPT_SSL_VERIFYPEER (https://curl.se/libcurl/c/CURLOPT_SSL_VERIFYPEER.html, source https://github.com/curl/curl/blob/curl-8_23_0/docs/libcurl/opts/CURLOPT_SSL_VERIFYPEER.md):
When CURLOPT_SSL_VERIFYPEER(3) is enabled, and the verification fails to prove that the certificate is signed by a CA, the connection fails.
CURLOPT_SSL_VERIFYHOST (https://curl.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html, source https://github.com/curl/curl/blob/curl-8_23_0/docs/libcurl/opts/CURLOPT_SSL_VERIFYHOST.md):
When CURLOPT_SSL_VERIFYHOST(3) is set to 1 or 2, the server certificate must indicate that it was made for the hostname or address curl connects to, or the connection fails.
Cause: lib/vtls/wolfssl.c lines 1717 to 1734 (curl-8_23_0) check for a missing peer certificate only when !connssl->peer.sni, the IP-literal path. On the hostname path curl relies on WOLFSSL_VERIFY_PEER and wolfSSL_check_domain_name(), and neither runs when no certificate is sent, so an anonymous suite passes. The same server through https://127.0.0.1/ is refused with "unable to get peer certificate", so curl itself expects the check to exist.
Anonymous suites are enabled by default in common wolfSSL packages: Debian libwolfssl (bookworm, trixie, sid) and the Homebrew bottle all define HAVE_ANON and OPENSSL_COMPATIBLE_DEFAULTS in options.h.
Affected version
curl 8.23.0 and master 7d091c71fd built with wolfSSL 5.9.4 (Homebrew bottle), macOS. libcurl and the curl tool. OpenSSL and GnuTLS builds are not affected.
Steps To Reproduce:
-
Start a TLS 1.2 server with no certificate (Python ssl,
ctx.set_ciphers('ADH-AES128-SHA:@SECLEVEL=0'),ctx.load_dh_params(...), noload_cert_chain) on 127.0.0.1:34500 that logs the request headers it receives. -
Run curl built with wolfSSL:
curl --cacert ca.pem -u alice:S3cret -w 'http=%{http_code} verify=%{ssl_verify_result}\n' https://localhost:34500/
- Result:
http=200 verify=0
-v: SSL connection using TLSv1.2 / ADH-AES128-SHA
server log:
handshake ok cipher=ADH-AES128-SHA (server sent no certificate)
GET / HTTP/1.1
Authorization: Basic YWxpY2U6UzNjcmV0
Controls against the same server: OpenSSL build curl: (35) ... handshake failure (nothing sent), GnuTLS build curl: (35) GnuTLS, handshake failed, wolfSSL via https://127.0.0.1:34500/ gives curl: (60) unable to get peer certificate. A CA-signed server still returns 200 and an untrusted-CA server is refused with rc=35.
Proposed fix (verified: anonymous server refused with exit 60 and receives no request, CA-signed server still 200):
--- a/lib/vtls/wolfssl.c
+++ b/lib/vtls/wolfssl.c
@@ -1733,6 +1733,16 @@ static CURLcode wssl_connect(struct Curl_cfilter *cf,
wolfSSL_X509_free(cert);
}
+ if(ret == WOLFSSL_SUCCESS &&
+ (conn_config->verifypeer || conn_config->verifyhost)) {
+ WOLFSSL_X509 *cert = wolfSSL_get_peer_certificate(wssl->ssl);
+ if(!cert) {
+ failf(data, "SSL: could not get peer certificate");
+ return CURLE_PEER_FAILED_VERIFICATION;
+ }
+ wolfSSL_X509_free(cert);
+ }
+
if(ret == WOLFSSL_SUCCESS) {
return CURLE_OK;
}
Impact
An attacker on the network path answers the TLS handshake of a normal curl -u user:pass https://api.example.com/, or of any default-option libcurl application, with an anonymous-DH ServerHello and no certificate. curl built with wolfSSL completes the connection and sends the credentials and request body to the attacker: a full certificate verification bypass with no user error involved.