HexBrain

curl · Report #3969300

CVE-2026-80230: OpenSSL pinning bypass

Validstatus
6upvotes
3 Sept 2026disclosed

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 (or CURLOPT_SSL_VERIFYPEER=0, CURLOPT_SSL_VERIFYHOST=0)
  • Client sets --pinnedpubkey with 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

  1. Client connects with --insecure and --pinnedpubkey <hash>, negotiating an anonymous cipher.
  2. Curl_ossl_check_peer_cert() calls SSL_get1_peer_certificate(octx->ssl) → returns NULL (line 4795).
  3. Guard at line 4798: !(conn_config->verifypeer || conn_config->verifyhost) → TRUE because --insecure disables both.
  4. goto out (line 4799) jumps to line 4876, skipping ossl_check_pinned_key() at line 4874.
  5. result is still CURLE_OK → function returns success.
  6. 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.

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