curl · Report #3969300
CVE-2026-80230: OpenSSL pinning bypass
How it was found
Summary
Curl_ossl_check_peer_cert() (line 4795) calls SSL_get1_peer_certificate(). When no peer certificate is available — as with anonymous cipher suites like AECDH-AES128-SHA — the function returns NULL. The subsequent guard at lines 4798–4799 checks !(conn_config->verifypeer || conn_config->verifyhost); when both are disabled (--insecure), it executes goto out, jumping past ossl_check_pinned_key() at line 4874 and returning CURLE_OK.
This means a user who specifies --insecure --pinnedpubkey <hash> expects certificate verification to be relaxed but public-key pinning to remain enforced. Instead, the pin check is silently skipped when the server presents no certificate.
Preconditions
- libcurl built with OpenSSL
- Client sets
--insecure(orCURLOPT_SSL_VERIFYPEER=0,CURLOPT_SSL_VERIFYHOST=0) - Client sets
--pinnedpubkeywith a specific hash - Client allows an anonymous cipher suite (e.g.
--ciphers AECDH-AES128-SHA:@SECLEVEL=0) - TLS version ≤ 1.2 (
--tls-max 1.2) — TLS 1.3 does not support anonymous authentication - Server configured with matching anonymous cipher (no certificate loaded)
Reproduction
The PoC (poc.py) starts a Python TLS server with AECDH-AES128-SHA (no certificate), then invokes:
/opt/curl/bin/curl --insecure \
--pinnedpubkey "sha256//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" \
--ciphers "AECDH-AES128-SHA:@SECLEVEL=0" \
--tls-max 1.2 \
https://127.0.0.1:PORT/
Success criterion: curl exits 0 and receives the HTTP response. Expected behavior: CURLE_SSL_PINNEDPUBKEYNOTMATCH (exit 90).
Negative control: Same wrong pin against a server with a real certificate → curl exits 90. This confirms the pin check functions when a certificate is present.
Root cause trace
- Client connects with
--insecureand--pinnedpubkey <hash>, negotiating an anonymous cipher. Curl_ossl_check_peer_cert()callsSSL_get1_peer_certificate(octx->ssl)→ returns NULL (line 4795).- Guard at line 4798:
!(conn_config->verifypeer || conn_config->verifyhost)→ TRUE because--insecuredisables both. goto out(line 4799) jumps to line 4876, skippingossl_check_pinned_key()at line 4874.resultis stillCURLE_OK→ function returns success.- curl completes the transfer without checking the public-key pin.
Remediation
Before the goto out at line 4799, check whether pinned_key is set. If a pin is configured but no certificate is available to verify it against, return CURLE_SSL_PINNEDPUBKEYNOTMATCH instead of silently succeeding. Alternatively, restructure the function to always reach ossl_check_pinned_key() when pinned_key is non-NULL, regardless of the verifypeer/verifyhost state.
Impact
An active MITM attacker who controls the network can force negotiation of an anonymous cipher (if the client allows it via --ciphers) and intercept traffic even though the client specified a public-key pin. The attack requires the client to use --insecure (or CURLOPT_SSL_VERIFYPEER=0) together with --pinnedpubkey, and the client must allow an anonymous cipher suite. While --insecure already weakens security, users who combine it with pinning rely on the pin as their sole authentication — this bypass removes that guarantee.