CVE-2022-27781: CERTINFO never-ending busy-loop
What the report claims
Curl is prone to a DoS attack in case the NSS TLS library is used and the CERTINFO option is enabled. Using maliciously crafted certificates on a server, an attacker can make curl run into an endless loop when connecting to the server.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2022-27781 CERTINFO never-ending busy-loop
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 while loop at lib/vtls/nss.c:1019-1028 traverses the certificate issuer chain without any cycle detection.
- If a server presents a certificate chain with a loop (mutual issuers), CERT_FindCertIssuer will keep returning certificates indefinitely.
- The only loop exit conditions are cert2 becoming NULL or cert2->isRoot being true; neither occurs in a cyclic chain.
- The vulnerability is reachable when CERTINFO is enabled (line 1009) and the NSS backend is used.
- An attacker controlling a TLS server can trigger this DoS against any libcurl application built with NSS and CERTINFO enabled.
How this analysis ran 6 claims · 14 citations verified
Checked against curl master on 2022-04-30 (6d8619337769). 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 and PoC. The infinite loop in display_conn_info() when processing a cyclic certificate chain is confirmed in lib/vtls/nss.c lines 1019-1028. The while loop lacks cycle detection, so a malicious server presenting mutually-issued CA certificates causes a DoS. We will add a visited-certificate check to bound the traversal.
Inspect all claims and source code 6 claims · 3 code excerpts
Report claims
display_conn_info() runs into an endless loop when the received certificate chain contains a loop
Read the reasoning
The while loop at lines 1019-1028 traverses the issuer chain using CERT_FindCertIssuer but maintains no visited-certificate set. If the chain contains a cycle (e.g., CA1 issued by CA2, CA2 issued by CA1), cert2 never becomes NULL and cert2->isRoot is never true, so the loop never terminates.
display_conn_info() starts with the leaf certificate and attempts to find its issuer certificate in the chain
Read the reasoning
The function starts with the peer (leaf) certificate at line 1005 and immediately attempts to find its issuer at line 1018.
certificate chain contains a loop
Read the reasoning
The loop condition at line 1019 and the repeated issuer lookup at line 1025 will follow a cyclic chain indefinitely because there is no cycle detection.
application runs into an infinite loop and consumes nearly 100% CPU
Read the reasoning
An infinite loop with no blocking calls (only CERT_FindCertIssuer and CERT_DestroyCertificate) will consume 100% CPU.
modify URL in certinfo example to point to https://localhost:4443/ and execute certinfo
Read the reasoning
The example enables CERTINFO and sets a URL; changing the URL to the malicious server reproduces the issue.
build curl with NSS TLS library and with examples
Read the reasoning
The vulnerable code is in the NSS-specific TLS backend (lib/vtls/nss.c), so building with --with-nss is required.
Deterministic checks that failed
- file lib/vtls/nss.c existed in curl's history but not at bc677a8f16a5
Read the original report (excerpt)
Source code
| 21 | ***************************************************************************/ |
| 22 | /* <DESC> |
| 23 | * Extract lots of TLS certificate info. |
| 24 | * </DESC> |
| 25 | */ |
| 26 | #include <stdio.h> |
| 27 | |
| 28 | #include <curl/curl.h> |
| 29 | |
| 30 | static size_t wrfu(void *ptr, size_t size, size_t nmemb, void *stream) |
| 31 | { |
| 32 | (void)stream; |
| 33 | (void)ptr; |
| 34 | return size * nmemb; |
| 35 | } |
| 36 | |
| 37 | int main(void) |
| 38 | { |
| 39 | CURL *curl; |
| 40 | CURLcode res; |
| 41 | |
| 42 | curl_global_init(CURL_GLOBAL_DEFAULT); |
| 43 | |
| 44 | curl = curl_easy_init(); |
| 45 | if(curl) { |
| 46 | curl_easy_setopt(curl, CURLOPT_URL, "https://www.example.com/"); |
| C5: curl_easy_setopt(curl, CURLOPT_URL, "https://www.example.com/"); -- the line to modify | |
| 47 | |
| 48 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, wrfu); |
| 49 | |
| 50 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); |
| 51 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); |
| 52 | |
| 53 | curl_easy_setopt(curl, CURLOPT_VERBOSE, 0L); |
| 54 | curl_easy_setopt(curl, CURLOPT_CERTINFO, 1L); |
| C5: curl_easy_setopt(curl, CURLOPT_CERTINFO, 1L); enables CERTINFO | |
| 55 | |
| 56 | res = curl_easy_perform(curl); |
| 57 | |
| 58 | if(!res) { |
| 59 | struct curl_certinfo *certinfo; |
| 60 | |
| 61 | res = curl_easy_getinfo(curl, CURLINFO_CERTINFO, &certinfo); |
| 62 | |
| 63 | if(!res && certinfo) { |
| 64 | int i; |
| 65 | |
| 66 | printf("%d certs!\n", certinfo->num_of_certs); |
| 67 | |
| 68 | for(i = 0; i < certinfo->num_of_certs; i++) { |
| 69 | struct curl_slist *slist; |
| 70 | |
| 71 | for(slist = certinfo->certinfo[i]; slist; slist = slist->next) |
| 986 | static CURLcode display_conn_info(struct Curl_easy *data, PRFileDesc *sock) |
| C6: display_conn_info is compiled only when NSS backend is used | |
| 987 | { |
| 988 | CURLcode result = CURLE_OK; |
| 989 | SSLChannelInfo channel; |
| 990 | SSLCipherSuiteInfo suite; |
| 991 | CERTCertificate *cert; |
| 992 | CERTCertificate *cert2; |
| 993 | CERTCertificate *cert3; |
| 994 | PRTime now; |
| 995 | |
| 996 | if(SSL_GetChannelInfo(sock, &channel, sizeof(channel)) == |
| 997 | SECSuccess && channel.length == sizeof(channel) && |
| 998 | channel.cipherSuite) { |
| 999 | if(SSL_GetCipherSuiteInfo(channel.cipherSuite, |
| 1000 | &suite, sizeof(suite)) == SECSuccess) { |
| 1001 | infof(data, "SSL connection using %s", suite.cipherSuiteName); |
| 1002 | } |
| 1003 | } |
| 1004 | |
| 1005 | cert = SSL_PeerCertificate(sock); |
| C2: cert = SSL_PeerCertificate(sock) obtains the leaf certificate | |
| 1006 | if(cert) { |
| 1007 | infof(data, "Server certificate:"); |
| 1008 | |
| 1009 | if(!data->set.ssl.certinfo) { |
| 1010 | display_cert_info(data, cert); |
| 1011 | CERT_DestroyCertificate(cert); |
| 1012 | } |
| 1013 | else { |
| 1014 | /* Count certificates in chain. */ |
| 1015 | int i = 1; |
| 1016 | now = PR_Now(); |
| 1017 | if(!cert->isRoot) { |
| 1018 | cert2 = CERT_FindCertIssuer(cert, now, certUsageSSLCA); |
| C2: cert2 = CERT_FindCertIssuer(cert, now, certUsageSSLCA) finds the leaf's issuer | |
| 1019 | while(cert2) { |
| C1: while(cert2) loop with no cycle detection; if certificate chain has a loop, cert2 never becomes NULL and cert2->isRoot never true | |
| C3: while(cert2) continues as long as an issuer is found | |
| C4: infinite while loop consumes CPU | |
| 1020 | i++; |
| C1: i++ increments on each iteration | |
| C4: i++ executes on every iteration with no bound | |
| 1021 | if(cert2->isRoot) { |
| C1: only exit condition is cert2->isRoot, which is never true in a mutual-issuer loop | |
| 1022 | CERT_DestroyCertificate(cert2); |
| 1023 | break; |
| 1024 | } |
| 1025 | cert3 = CERT_FindCertIssuer(cert2, now, certUsageSSLCA); |
| C1: cert3 = CERT_FindCertIssuer(cert2, ...) continues the chain traversal | |
| C3: CERT_FindCertIssuer called repeatedly on each successive certificate | |
| 1026 | CERT_DestroyCertificate(cert2); |
| 1027 | cert2 = cert3; |
| C1: cert2 = cert3 advances to next issuer without visited-set tracking | |
| 1028 | } |
| 1029 | } |
| 1030 | |
| 1031 | result = Curl_ssl_init_certinfo(data, i); |
| 1032 | if(!result) { |
| 1033 | for(i = 0; cert; cert = cert2) { |
| 1034 | result = Curl_extract_certinfo(data, i++, (char *)cert->derCert.data, |
| 1035 | (char *)cert->derCert.data + |
| 1036 | cert->derCert.len); |
| 1037 | if(result) |
| 1038 | break; |
| 1039 | |
| 1040 | if(cert->isRoot) { |
| 1041 | CERT_DestroyCertificate(cert); |
| 1042 | break; |
| 1043 | } |
| 1044 | |
| 1045 | cert2 = CERT_FindCertIssuer(cert, now, certUsageSSLCA); |
| 1046 | CERT_DestroyCertificate(cert); |
| 1047 | } |
| 1048 | } |
| 1049 | } |
| 1050 | } |
| 1051 | |
| 1052 | return result; |
| 1053 | } |
Show 44 lines (not cited by the judge)
| 38 | while(<STDIN>) { |
| 39 | if(/^ *CURLOPT\(([^,]*), ([^,]*), (\d+)\)/) { |
| 40 | my($opt, $type, $num)=($1,$2,$3); |
| 41 | my $name; |
| 42 | my $ext = $type; |
| 43 | |
| 44 | if($opt =~ /OBSOLETE/) { |
| 45 | # skip obsolete options |
| 46 | next; |
| 47 | } |
| 48 | |
| 49 | if($opt =~ /^CURLOPT_(.*)/) { |
| 50 | $name=$1; |
| 51 | } |
| 52 | $ext =~ s/CURLOPTTYPE_//; |
| 53 | $ext =~ s/CBPOINT/CBPTR/; |
| 54 | $ext =~ s/POINT\z//; |
| 55 | $type = "CURLOT_$ext"; |
| 56 | |
| 57 | $opt{$name} = $opt; |
| 58 | $type{$name} = $type; |
| 59 | push @names, $name; |
| 60 | if($num < $lastnum) { |
| 61 | print STDERR "ERROR: $opt has bad number\n"; |
| 62 | exit 2; |
| 63 | } |
| 64 | else { |
| 65 | $lastnum = $num; |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | # alias for an older option |
| 70 | # old = new |
| 71 | if(/^#define (CURLOPT_[^ ]*) *(CURLOPT_\S*)/) { |
| 72 | my ($o, $n)=($1, $2); |
| 73 | # skip obsolete ones |
| 74 | if($n !~ /OBSOLETE/) { |
| 75 | $o =~ s/^CURLOPT_//; |
| 76 | $n =~ s/^CURLOPT_//; |
| 77 | $alias{$o} = $n; |
| 78 | push @names, $o, |
| 79 | } |
| 80 | } |
| 81 | } |