✓ProveIt
Examples / Report #3969300 Saved analysis

CVE-2026-80230: OpenSSL pinning bypass

curl · 2026-08-2523.0s analysis · $0.023 in model calls

What the report claims

When using --insecure with --pinnedpubkey and an anonymous cipher suite, curl skips public-key pinning because it jumps past the pin check when no certificate is presented.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2026-80230 OpenSSL pinning bypass

Shown for comparison after analysis. The pipeline did not see this label.

ProveIt's finding

Code supports the claim

The code supports the reported flaw. A maintainer should review its impact and decide what to do next.

Why this finding?
  • The release version (curl-8_21_0) contains the exact flaw described: when no peer certificate is available and both verifypeer and verifyhost are disabled, the code jumps to 'out' without checking the pinned key.
  • The fix (commit 5267ed859d 'openssl: no server cert is only okay if also not pinned') is present in master (7be1e70cb6bc) but not in the latest release. The report correctly identifies a vulnerability in the released version.
  • The attack allows a MITM to bypass public-key pinning — a security control — when the user intentionally combines --insecure with --pinnedpubkey, which is a documented and supported configuration.
How this analysis ran 5 claims · 14 citations verified

Checked against curl master on 2026-08-25 (7be1e70cb6bc). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.

1 · Grounding
grounded
no model · 11 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 2122 tokens · $0.0002
3 · Evidence
5 code snippets
real curl source at 7be1e70cb6bc · 3 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
14776 tokens · 20.3s · $0.0231
5 · Receipts
14 of 14 citations verified
every citation must point at code the judge was shown
Review a draft reply to the reporter

Edit and verify before sending.

Thank you for the detailed report. The flaw you describe exists in the current release (curl-8.21.0): when using --insecure with --pinnedpubkey and an anonymous cipher suite, the pinned-key check is skipped because the guard at line 4767 lacks a pinned-key check. This has already been fixed in master by commit 5267ed859d ('openssl: no server cert is only okay if also not pinned'), which adds '&& !pinned(cf, data)' to the condition. The fix will be in the next release. We appreciate the clear reproduction steps and root-cause analysis.

Inspect all claims and source code 5 claims · 5 code excerpts

Report claims

C1flaw supported

Curl_ossl_check_peer_cert() returns NULL when no peer certificate is available, and the guard !(conn_config->verifypeer || conn_config->verifyhost) evaluates TRUE when both are disabled, causing a goto out that skips ossl_check_pinned_key()

Read the reasoning

In the release version (curl-8_21_0), when SSL_get1_peer_certificate returns NULL and both verifypeer and verifyhost are disabled, the code executes 'goto out' at line 4768, jumping to line 4847 and skipping ossl_check_pinned_key at line 4845. The guard does not check whether a pinned key is configured.

C2location supported

lines 4795, 4798-4799, 4874, 4876 in the OpenSSL backend of libcurl

Read the reasoning

The line numbers in the report (4795, 4798-4799, 4874, 4876) correspond approximately to lines 4764, 4767, 4845, 4847 in the release version, which is what the reporter read. The master version has different line numbers due to intervening commits.

C3trigger supported

client uses --insecure (or CURLOPT_SSL_VERIFYPEER=0 and CURLOPT_SSL_VERIFYHOST=0) together with --pinnedpubkey and an anonymous cipher suite such as AECDH-AES128-SHA

Read the reasoning

The trigger requires --insecure (disabling verifypeer and verifyhost), --pinnedpubkey (setting pinned key), and an anonymous cipher suite (causing SSL_get1_peer_certificate to return NULL). All three map to the code conditions.

C4impact supported

public-key pinning is silently bypassed, allowing an active MITM to intercept traffic even when a pin is configured

Read the reasoning

When the goto out is taken, result remains CURLE_OK and the function returns success without ever calling ossl_check_pinned_key, so the pin is silently ignored.

C5reproduction supported

run a TLS server with AECDH-AES128-SHA (no certificate) and invoke curl with --insecure --pinnedpubkey <hash> --ciphers AECDH-AES128-SHA:@SECLEVEL=0 --tls-max 1.2 https://127.0.0.1:PORT/

Read the reasoning

The reproduction steps match the code path: anonymous cipher (AECDH-AES128-SHA) → no server cert → --insecure disables both verification flags → guard passes → pin check skipped → CURLE_OK returned.

Read the original report (excerpt)

Full report on HackerOne ↗

## 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: ```sh /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 w …

Source code

lib/vtls/openssl.c@curl-8_21_0lines 4740–4850 · definition of Curl_ossl_check_peer_cert as in release curl-8_21_0; differs from master; commits that changed it since: 7be1e70cb6 vtls: move 'native_ca_store' ssl_config_data => ssl_primary_config | 5267ed859d openssl: no server cert is only okay if also not pinned | 75c2c881af openssl+sectrust: fix session reuse
4740CURLcode Curl_ossl_check_peer_cert(struct Curl_cfilter *cf,
4741 struct Curl_easy *data,
4742 struct ossl_ctx *octx,
4743 struct ssl_peer *peer)
4744{
4745 struct connectdata *conn = cf->conn;
4746 struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data);
4747 struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
4748 CURLcode result = CURLE_OK;
C4: result = CURLE_OK initialized and returned
4749 long ossl_verify;
4750 X509 *server_cert;
4751 bool verified = FALSE;
4752#if !defined(OPENSSL_NO_OCSP) && defined(USE_APPLE_SECTRUST)
4753 bool sectrust_verified = FALSE;
4754#endif
4755
4756 if(data->set.ssl.certinfo && !octx->reused_session) {
4757 /* asked to gather certificate info. Reused sessions do not have cert
4758 chains */
4759 result = ossl_certchain(data, octx->ssl);
4760 if(result)
4761 return result;
4762 }
4763
4764 server_cert = SSL_get1_peer_certificate(octx->ssl);
C2: SSL_get1_peer_certificate call
C5: SSL_get1_peer_certificate returns NULL for anonymous ciphers
4765 if(!server_cert) {
C1: server_cert = SSL_get1_peer_certificate(octx->ssl); returns NULL when no peer certificate
4766 /* no verification at all, this maybe acceptable */
4767 if(!(conn_config->verifypeer || conn_config->verifyhost))
C1: if(!(conn_config->verifypeer || conn_config->verifyhost)) goto out; — guard lacks pinned check
C2: guard condition
C3: condition checks conn_config->verifypeer and conn_config->verifyhost
C5: guard allows goto out when verifypeer and verifyhost are both false
4768 goto out;
C4: goto out skips pinned key check
4769
4770 failf(data, "SSL: could not get peer certificate");
4771 result = CURLE_PEER_FAILED_VERIFICATION;
4772 goto out;
4773 }
4774
4775#ifdef CURLVERBOSE
4776 result = ossl_infof_cert(cf, data, server_cert);
4777 if(result)
4778 goto out;
4779 infof_certstack(data, octx->ssl);
4780#endif
4781
4782 if(conn_config->verifyhost) {
4783 result = ossl_verifyhost(data, conn, peer, server_cert);
4784 if(result)
4785 goto out;
4786 }
4787 /* `verifyhost` is either OK or not requested from here on */
4788
4789 ossl_verify = SSL_get_verify_result(octx->ssl);
4790 ssl_config->certverifyresult = ossl_verify;
4791 infof(data, "OpenSSL verify result: %lx", (unsigned long)ossl_verify);
4792
4793 verified = (ossl_verify == X509_V_OK);
4794 if(verified)
4795 infof(data, "SSL certificate verified via OpenSSL.");
4796
4797#ifdef USE_APPLE_SECTRUST
4798 if(!verified && conn_config->verifypeer && ssl_config->native_ca_store) {
4799 /* we verify using Apple SecTrust *unless* OpenSSL already verified.
4800 * This may happen if the application intercepted the OpenSSL callback
4801 * and installed its own. */
4802 result = ossl_apple_verify(cf, data, octx, peer, &verified);
4803 if(result && (result != CURLE_PEER_FAILED_VERIFICATION))
4804 goto out; /* unexpected error */
4805 if(verified) {
4806 infof(data, "SSL certificate verified via Apple SecTrust.");
4807 ssl_config->certverifyresult = X509_V_OK;
4808#ifndef OPENSSL_NO_OCSP
4809 sectrust_verified = TRUE;
4810#endif
4811 }
4812 }
4813#endif
4814
4815 if(!verified) {
4816 /* no trust established, report the OpenSSL status */
4817 if(conn_config->verifypeer) {
4818 failf(data, "SSL certificate OpenSSL verify result: %s (%ld)",
4819 X509_verify_cert_error_string(ossl_verify), ossl_verify);
4820 result = CURLE_PEER_FAILED_VERIFICATION;
4821 goto out;
4822 }
4823 infof(data, " SSL certificate verification failed, continuing anyway!");
4824 }
4825
4826#ifndef OPENSSL_NO_OCSP
4827 if(conn_config->verifystatus &&
4828#ifdef USE_APPLE_SECTRUST
4829 !sectrust_verified && /* already verified via apple sectrust, cannot
4830 * verifystate via OpenSSL in that case as it
4831 * does not have the trust anchors */
4832#endif
4833 !octx->reused_session) {
4834 /* do not do this after Session ID reuse */
4835 result = verifystatus(cf, data, octx);
4836 if(result)
4837 goto out;
4838 }
4839#endif
4840
4841 result = ossl_check_issuer(cf, data, server_cert);
4842 if(result)
4843 goto out;
4844
4845 result = ossl_check_pinned_key(cf, data, server_cert);
C1: ossl_check_pinned_key() is at line 4845, after the 'out:' label at 4847, so goto out skips it
C2: ossl_check_pinned_key call
C4: ossl_check_pinned_key is never reached
4846
4847out:
C2: out: label
4848 X509_free(server_cert);
4849 return result;
4850}
lib/vtls/openssl.clines 4611–4623 · definition of ossl_check_pinned_key
4611static CURLcode ossl_check_pinned_key(struct Curl_cfilter *cf,
4612 struct Curl_easy *data,
4613 X509 *server_cert)
4614{
4615 CURLcode result = CURLE_OK;
4616 const char *ptr = pinned(cf, data);
C3: pinned(cf, data) retrieves the pinned public key setting
4617 if(ptr) {
4618 result = ossl_pkp_pin_peer_pubkey(data, server_cert, ptr);
4619 if(result)
4620 failf(data, "SSL: public key does not match pinned public key");
4621 }
4622 return result;
4623}
lib/vtls/openssl.clines 4781–4887 · definition of Curl_ossl_check_peer_cert
Show 107 lines (not cited by the judge)
4781CURLcode Curl_ossl_check_peer_cert(struct Curl_cfilter *cf,
4782 struct Curl_easy *data,
4783 struct ossl_ctx *octx,
4784 struct ssl_peer *peer)
4785{
4786 struct connectdata *conn = cf->conn;
4787 struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data);
4788 struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
4789 CURLcode result = CURLE_OK;
4790 long ossl_verify;
4791 X509 *server_cert;
4792 bool verified = FALSE;
4793
4794 if(data->set.ssl.certinfo && !octx->reused_session) {
4795 /* asked to gather certificate info. Reused sessions do not have cert
4796 chains */
4797 result = ossl_certchain(data, octx->ssl);
4798 if(result)
4799 return result;
4800 }
4801
4802 server_cert = SSL_get1_peer_certificate(octx->ssl);
4803 if(!server_cert) {
4804 /* no verification at all, this maybe acceptable */
4805 if(!(conn_config->verifypeer || conn_config->verifyhost) &&
4806 !pinned(cf, data))
4807 goto out;
4808
4809 failf(data, "SSL: could not get peer certificate");
4810 result = CURLE_PEER_FAILED_VERIFICATION;
4811 goto out;
4812 }
4813
4814#ifdef CURLVERBOSE
4815 result = ossl_infof_cert(cf, data, server_cert);
4816 if(result)
4817 goto out;
4818 infof_certstack(data, octx->ssl);
4819#endif
4820
4821 if(conn_config->verifyhost) {
4822 result = ossl_verifyhost(data, conn, peer, server_cert);
4823 if(result)
4824 goto out;
4825 }
4826 /* `verifyhost` is either OK or not requested from here on */
4827
4828 ossl_verify = SSL_get_verify_result(octx->ssl);
4829 ssl_config->certverifyresult = ossl_verify;
4830 infof(data, "OpenSSL verify result: %lx", (unsigned long)ossl_verify);
4831
4832 verified = (ossl_verify == X509_V_OK);
4833 if(verified)
4834 infof(data, "SSL certificate verified via OpenSSL.");
4835
4836#ifdef USE_APPLE_SECTRUST
4837 if(!verified && conn_config->verifypeer && conn_config->native_ca_store) {
4838 /* we verify using Apple SecTrust *unless* OpenSSL already verified.
4839 * This may happen if the application intercepted the OpenSSL callback
4840 * and installed its own. */
4841 result = ossl_apple_verify(cf, data, octx, peer);
4842 if(result && (result != CURLE_PEER_FAILED_VERIFICATION))
4843 goto out; /* unexpected error */
4844 if(octx->sectrust_verified) {
4845 infof(data, "SSL certificate verified via Apple SecTrust.");
4846 ssl_config->certverifyresult = X509_V_OK;
4847 verified = TRUE;
4848 }
4849 }
4850#endif
4851
4852 if(!verified) {
4853 /* no trust established, report the OpenSSL status */
4854 if(conn_config->verifypeer) {
4855 failf(data, "SSL certificate OpenSSL verify result: %s (%ld)",
4856 X509_verify_cert_error_string(ossl_verify), ossl_verify);
4857 result = CURLE_PEER_FAILED_VERIFICATION;
4858 goto out;
4859 }
4860 infof(data, " SSL certificate verification failed, continuing anyway!");
4861 }
4862
4863#ifndef OPENSSL_NO_OCSP
4864 if(conn_config->verifystatus &&
4865#ifdef USE_APPLE_SECTRUST
4866 !octx->sectrust_verified && /* already verified via sectrust, cannot
4867 * verifystate via OpenSSL in that case as it
4868 * does not have the trust anchors */
4869#endif
4870 !octx->reused_session) {
4871 /* do not do this after Session ID reuse */
4872 result = verifystatus(cf, data, octx);
4873 if(result)
4874 goto out;
4875 }
4876#endif
4877
4878 result = ossl_check_issuer(cf, data, server_cert);
4879 if(result)
4880 goto out;
4881
4882 result = ossl_check_pinned_key(cf, data, server_cert);
4883
4884out:
4885 X509_free(server_cert);
4886 return result;
4887}
lib/vtls/openssl.hlines 166–166 · definition of SSL_get1_peer_certificate
Show 1 lines (not cited by the judge)
166#define SSL_get1_peer_certificate SSL_get_peer_certificate
lib/vtls/openssl.c@curl-8_21_0lines 4571–4592 · definition of ossl_check_pinned_key as in release curl-8_21_0; differs from master; commits that changed it since: 5267ed859d openssl: no server cert is only okay if also not pinned
Show 22 lines (not cited by the judge)
4571static CURLcode ossl_check_pinned_key(struct Curl_cfilter *cf,
4572 struct Curl_easy *data,
4573 X509 *server_cert)
4574{
4575 const char *ptr;
4576 CURLcode result = CURLE_OK;
4577
4578 (void)cf;
4579#ifndef CURL_DISABLE_PROXY
4580 ptr = Curl_ssl_cf_is_proxy(cf) ?
4581 data->set.str[STRING_SSL_PINNEDPUBLICKEY_PROXY] :
4582 data->set.str[STRING_SSL_PINNEDPUBLICKEY];
4583#else
4584 ptr = data->set.str[STRING_SSL_PINNEDPUBLICKEY];
4585#endif
4586 if(ptr) {
4587 result = ossl_pkp_pin_peer_pubkey(data, server_cert, ptr);
4588 if(result)
4589 failf(data, "SSL: public key does not match pinned public key");
4590 }
4591 return result;
4592}