HexBrain

curl · Report #4118610

CVE-2026-111484: OpenSSL with SecTrust accepts bad OCSP staples

Validstatus
mediumseverity
4upvotes
26 Oct 2026disclosed

How it was found

Summary:

The CVE-2026-7009 fix (51905671e0) made a missing staple fatal when Apple SecTrust verifies the chain, but when a staple is present curl still skips its own verifystatus() (lib/vtls/openssl.c:4864-4873, !octx->sectrust_verified) and leaves the staple to SecTrust. SecTrust evaluates it (SecTrustSetOCSPResponse(), lib/vtls/apple.c:229) under curl's best-effort revocation policy (apple.c:154 kSecRevocationUseAnyAvailableMethod; kSecRevocationRequirePositiveResponse is in the #if 0 at apple.c:155-167), so a staple it cannot validate counts as "no information" and the handshake succeeds. With --ca-native --cert-status, curl 8.23.0 accepts a staple with a forged signature, a stale "good" staple (nextUpdate 20 days in the past) and an "unknown" staple, while the same binary with --cacert, where OpenSSL verifies, rejects all three. Only a missing staple and a correctly signed "revoked" staple are refused, and Homebrew's released curl 8.22.0 behaves the same. The #if 0 comment's reason for best effort, that positive OCSP answers are not generally available for internet certificates, does not apply here: the user has demanded a stapled response, so a positive answer is exactly what --cert-status requires.

Affected version

8.20.0 through 8.23.0 and master (780405b584). Tested on 8.23.0 built from the curl-8_23_0 tag and on the Homebrew 8.22.0 release; 8.20.0 and 8.21.0 contain the same code. macOS 27.0.1, arm64:

curl 8.23.0-DEV (Darwin) libcurl/8.23.0-DEV OpenSSL/3.6.4 zlib/1.2.12
Features: alt-svc AppleSecTrust AsynchDNS HSTS HTTPS-proxy IPv6 Largefile libz SSL threadsafe UnixSockets
curl 8.22.0 (aarch64-apple-darwin25.6.0) libcurl/8.22.0 OpenSSL/3.6.4 zlib/1.2.12 brotli/1.2.0 zstd/1.5.7 AppleIDN libpsl/0.23.3 libssh2/1.11.1 nghttp2/1.70.0 ngtcp2/1.25.0 nghttp3/1.18.0 mit-krb5/1.7-prerelease OpenLDAP/2.4.28/Apple
Features: alt-svc AppleSecTrust AsynchDNS brotli GSS-API HSTS HTTP2 HTTP3 HTTPS-proxy IDN IPv6 Kerberos Largefile libz PSL SPNEGO SSL threadsafe UnixSockets zstd

The 8.23.0 build uses -DCURL_USE_OPENSSL=ON -DUSE_APPLE_SECTRUST=ON -DCURL_CA_BUNDLE=none -DCURL_CA_PATH=none.

Steps To Reproduce:

All on 127.0.0.1, scripts in the attached zip under poc/.

  1. python3 poc/mkpki2.py poc/pki2 creates root, intermediate and server "localhost", plus staples signed by the intermediate (good, unknown, revoked) and two bad ones (forged signature, stale).
  2. Trust the test root, as in #3694390: security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db poc/pki2/root.pem
  3. openssl s_server -accept 127.0.0.1:23003 -cert poc/pki2/server.pem -key poc/pki2/server.key -cert_chain poc/pki2/chain.pem -status_file poc/pki2/ocsp-stale-good.der -tls1_3 -www
  4. curl -v --ca-native --cert-status https://localhost:23003/ logs "SSL certificate verified via Apple SecTrust." and returns HTTP 200.
  5. curl --cacert poc/pki2/root.pem --cert-status https://localhost:23003/ fails with (91) OCSP response has expired.
  6. Clean up: security delete-certificate -c "Verify Test Root CA" ~/Library/Keychains/login.keychain-db

poc/matrix.sh runs every staple. Same 8.23.0 binary, TLS 1.3, no hooks of any kind:

stapled responseSecTrust verifies (--ca-native)OpenSSL verifies (--cacert)
none(91) No OCSP response received(91) No OCSP response received
good200200
revoked(60)(91) revocation reason: keyCompromise
forged signature200(91) OCSP response verification failed
stale200(91) OCSP response has expired
unknown200(91) status verification FAILED

Raw output for TLS 1.3, TLS 1.2 and the Homebrew binary: 01-keychain-trust-no-interposer.txt. Verbose trace: 02-verbose-stale-staple-trace.txt.

Fix

When verifystatus is set, add kSecRevocationRequirePositiveResponse to the revocation policy in Curl_vtls_apple_verify(); the best-effort default stays for every other transfer. With only that flag added (poc/requirepos_interpose.c, 03-fix-check-require-positive-response.txt) the same binary accepts the good staples and rejects unknown, revoked, forged, stale and missing ones.

AI disclosure: I used an AI assistant (Claude) for code review, test setup and drafting. I reproduced every result myself on curl built from the curl-8_23_0 tag and on the Homebrew 8.22.0 release; the attached logs are the raw output.

Impact

--cert-status is documented to fail when the stapled response is invalid, for example expired, or does not confirm the certificate. With SecTrust verifying, it only catches a missing staple and a correctly signed "revoked" one. An attacker holding the key of a revoked certificate defeats it by stapling an old "good" response captured before the revocation, or any self-signed "good" response, which is the attack OCSP stapling enforcement exists to stop. The conditions are those of CVE-2026-7009 (Medium): an OpenSSL-family build with Apple SecTrust where SecTrust verifies the chain, which is every such build without its own CA bundle and, with Homebrew's curl, every certificate chaining to a root trusted only in the macOS keychain, such as an enterprise CA.

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