CVE-2026-80230: OpenSSL pinning bypass
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
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.
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
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.
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.
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.
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.
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)
Source code
| 4740 | CURLcode 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 | |
| 4847 | out: |
| C2: out: label | |
| 4848 | X509_free(server_cert); |
| 4849 | return result; |
| 4850 | } |
| 4611 | static 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 | } |
Show 107 lines (not cited by the judge)
| 4781 | CURLcode 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 | |
| 4884 | out: |
| 4885 | X509_free(server_cert); |
| 4886 | return result; |
| 4887 | } |
Show 1 lines (not cited by the judge)
| 166 | #define SSL_get1_peer_certificate SSL_get_peer_certificate |
Show 22 lines (not cited by the judge)
| 4571 | static 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 | } |