curl · Report #4116076
CVE-2026-111483: GnuTLS accepts non-successful OCSP staples
How it was found
Summary
In curl 8.23.0 built with GnuTLS, --cert-status (CURLOPT_SSL_VERIFYSTATUS) only checks that some stapled OCSP response was received. It no longer checks the response status or the certificate status inside it. A server presenting a revoked certificate can staple a bogus, unsigned 5-byte OCSP response and curl completes the transfer. This brings back CVE-2024-8096, which was fixed in 2024.
Note: this report was prepared with AI assistance for the testing and code review. The writing and the findings are mine.
What the docs promise
docs/cmdline-opts/cert-status.md:
If this option is enabled and the server sends an invalid (e.g. expired) response, if the response suggests that the server certificate has been revoked, or no response at all is received, the verification fails.
docs/libcurl/opts/CURLOPT_SSL_VERIFYSTATUS.md says the same. So a revoked status, or an unusable response, must fail the transfer. It does not.
Root cause
lib/vtls/gtls.c, gtls_verify_ocsp_status() (line 1430 in curl-8_23_0). The whole check is now:
rc = gnutls_ocsp_status_request_get(session, &status_request);
if(rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) {
failf(data, "No OCSP response received");
return CURLE_SSL_INVALIDCERTSTATUS;
}
else if(rc < 0) {
failf(data, "Invalid OCSP response received");
return CURLE_SSL_INVALIDCERTSTATUS;
}
return CURLE_OK;
It returns CURLE_OK as soon as a staple is present, whatever it contains. The response is never imported or inspected.
This was introduced by commit 5e727ca855 ("GnuTLS: remove our own OCSP signature verification", PR #23312), which shipped in 8.23.0. That commit removed the status and certStatus checks added by aeb1a281ca (the CVE-2024-8096 fix). The comment in the code assumes gnutls_certificate_verify_peers2() already accounts for the stapled response, but as the PoC shows it does not reject a revoked certificate when the staple is a non-successful or unknown response.
Proof of concept
Local only. One server on 127.0.0.1, serving a certificate that is signed by the test CA but marked revoked (keyCompromise), stapling a bogus unsigned OCSPResponse whose responseStatus is tryLater:
printf '\x30\x03\x0a\x01\x03' > trylater.der
openssl s_server -quiet -naccept 20 -accept 127.0.0.1:30350 -tls1_2 \
-cert rev.pem -key rev.key -status_file trylater.der -www &
curl --cacert ca.pem --cert-status \
--resolve localhost:30350:127.0.0.1 https://localhost:30350/
Result on 8.23.0 GnuTLS:
RESULT: exit=0 http=200
The transfer succeeds against a revoked certificate.
Controls, same build and setup:
good certificate + good staple exit=0 (correct)
revoked certificate + no staple exit=91 No OCSP response received
revoked certificate + signed revoked exit=60 certificate error
OpenSSL backend, same bogus staple, refuses correctly:
curl: (91) Invalid OCSP response status: trylater (3)
A CA-signed response with certStatus=unknown is also accepted (exit=0) where it must fail.
Affected
curl 8.22.0 and 8.23.0 with GnuTLS, and current master (gtls.c is unchanged on master). curl 8.21.0 and earlier reject the bogus staple (exit 91). Reproduces on TLS 1.2 and TLS 1.3, with the curl tool and the libcurl API.
Proposed fix
Import the stapled response and require a successful response with certStatus good, restoring the check that 5e727ca855 removed:
if(rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) {
failf(data, "No OCSP response received");
return CURLE_SSL_INVALIDCERTSTATUS;
}
else if(rc < 0) {
failf(data, "Invalid OCSP response received");
return CURLE_SSL_INVALIDCERTSTATUS;
}
+ else {
+ gnutls_ocsp_resp_t resp;
+ gnutls_ocsp_cert_status_t status = GNUTLS_OCSP_CERT_UNKNOWN;
+
+ if(gnutls_ocsp_resp_init(&resp) < 0) {
+ failf(data, "Invalid OCSP response received");
+ return CURLE_SSL_INVALIDCERTSTATUS;
+ }
+ if(gnutls_ocsp_resp_import(resp, &status_request) < 0 ||
+ gnutls_ocsp_resp_get_single(resp, 0, NULL, NULL, NULL, NULL,
+ &status, NULL, NULL, NULL, NULL) < 0) {
+ gnutls_ocsp_resp_deinit(resp);
+ failf(data, "Invalid OCSP response received");
+ return CURLE_SSL_INVALIDCERTSTATUS;
+ }
+ gnutls_ocsp_resp_deinit(resp);
+ if(status != GNUTLS_OCSP_CERT_GOOD) {
+ failf(data, "Server certificate OCSP status not good (%d)", (int)status);
+ return CURLE_SSL_INVALIDCERTSTATUS;
+ }
+ }
return CURLE_OK;
}
With this patch the good staple still gives exit 0, and the bogus tryLater, unauthorized and unknown staples all give exit 91, matching the OpenSSL backend. A full fix may also want to reject responses whose thisUpdate/nextUpdate window is invalid, as the pre-regression code did.
Impact
A network attacker holding a revoked (but still CA-signed) certificate for the target host can defeat --cert-status / CURLOPT_SSL_VERIFYSTATUS on GnuTLS builds by stapling a bogus OCSP response, so curl accepts the revoked certificate and proceeds with the transfer, exactly the revocation check the option exists to enforce.