✓ProveIt
Examples / Report #2629968 Saved analysis

CVE-2024-7264: ASN.1 date parser overread

curl · 2024-07-3049.7s analysis · $0.044 in model calls

What the report claims

A specially-crafted certificate causes a stack-buffer-overflow in curl's ASN.1 date parser when using gnutls, leading to out-of-bounds reads during certificate parsing.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2024-7264 ASN.1 date parser overread

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

ProveIt's finding

Needs a policy review

The code does what the report says, but curl's own policy may exclude it. A maintainer decides.

curl policy: NULL dereferences and crashes
Judgment withdrawn. The answer failed the evidence check.
See the withdrawn model reasoning

The following reasoning failed the citation check and does not support the final finding.

  • The code confirms a real buffer over-read in GTime2str when a certificate ends with a fractional indicator ('.' or ',') and no digits: fracl becomes SIZE_MAX, casts to -1 for %.*s, causing strlen(fracp) to read past the buffer.
  • However, curl's documented policy explicitly states: 'If a malicious server can trigger a NULL dereference in curl or otherwise cause curl to crash (and nothing worse), chances are big that we do not consider that a security problem.' This is a crash (DoS) from a malicious server certificate with no demonstrated data leak or code execution.
  • The report itself notes: 'I couldn't see a way where the remote attacker can actually get the content of the over-read memory bytes.'
How this analysis ran 5 claims · 17 citations verified

Evidence check: the behavior is real; the project's policy section 'NULL dereferences and crashes' may exclude it

Checked against curl tested commit 2a59c8d4cebf (2a59c8d4cebf). Test evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.

1 · Grounding
partially grounded
no model · 7 references checked out, 1 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 3651 tokens · $0.0003
3 · Evidence
3 code snippets
real curl source at 2a59c8d4cebf · 3 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
21862 tokens · 37.1s · $0.0440
5 · Receipts
17 of 17 citations verified
every citation must point at code the judge was shown
Inspect the withdrawn draft reply

This draft belongs to the withdrawn judgment. It may contain unsupported claims.

Thank you for the detailed report and reproduction case. The code confirms the buffer over-read in GTime2str when a certificate ends with '.' or ',' and no fractional digits: fracl wraps to SIZE_MAX, casts to -1 for the %.*s format, and strlen reads past the buffer. However, per curl's vulnerability disclosure policy (section 'NULL dereferences and crashes'), a crash triggered by a malicious server certificate—without data leakage or code execution—is not treated as a security vulnerability. We would still fix the bug as a robustness issue, but it does not qualify for a CVE or security advisory. If you can demonstrate a path to information disclosure or code execution, please share those details.

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

This model judgment was withdrawn. The claim assessments below are preserved for inspection and may be wrong.

Report claims

C1flaw supported

The loop in GTime2str may set fracl to -1 when tzp == fracp, causing Curl_dyn_addf to treat it as 'no length given' and run strlen(tzp) beyond the buffer

Read the reasoning

When a certificate ends with '.' or ',' (fractional indicator) and no digits follow, tzp==fracp at line 525, making fracl = SIZE_MAX. Cast to int for %.*s gives -1, which formatf treats as 'no precision' and calls strlen(fracp). Since fracp==end (just past the certificate buffer), strlen reads out of bounds. The loop at line 525 also reads fracp[SIZE_MAX-1], but the observed crash is in strlen from the format call.

C2location supported

The flaw occurs in lib/vtls/x509asn1.c at line 525 in the loop initializing fracl

Read the reasoning

The loop at line 525 is where fracl becomes SIZE_MAX when tzp==fracp. This line is correctly identified as the origin of the oversized fracl value.

C3trigger supported

A specially-crafted certificate causes tzp to equal fracp, setting fracl to -1

Read the reasoning

A certificate where the fractional indicator ('.' or ',') is the last byte causes fracp to advance to end, tzp to advance to end (one increment), making tzp==fracp==end at line 525, so fracl = SIZE_MAX.

C4impact supported

The out-of-bounds strlen(tzp) read overflows the certificate buffer, causing a stack-buffer-overflow

Read the reasoning

The crash is in strlen(fracp) (not strlen(tzp) as the report states) when %.*s receives -1 precision. The AddressSanitizer stack trace confirms strlen reading 4471 bytes starting just past the stack buffer (offset 8224), matching fracp==end.

C5reproduction not verifiable

Compile curl with gnutls and AddressSanitizer, then run ./poc bad_cert_1.bin to trigger the overflow

Read the reasoning

Reproduction steps involve external files (poc.c, bad_cert_1.bin) not provided in the source code. The deterministic checks confirm the relevant functions exist at the tested revision, but the PoC cannot be verified from the given material.

Deterministic checks that failed

  • quote only 0/2 quoted lines appear anywhere in curl at 2a59c8d4cebf
Read the original report (excerpt)

Full report on HackerOne ↗

## Summary: When a specially-crafted certificate is passed to `Curl_extract_certinfo` to parse, it may read bytes beyond the end of the buffer in which the certificate is held. According to the application, this may be a stack read overflow or a heap read overflow. Specifically the issue is in function `GTime2str`, in which the specially-crafted input may cause it to set `fracl = -1` and then pass it to `Curl_dyn_addf`, which in turn treats this `-1` as "no length given" and goes on to run `strlen(tzp)` which goes beyond the end of the certificate buffer (assuming there are no null bytes). I believe the issue is in this loop (in `lib/vtls/x509asn1.c`): ``` 524 /* Strip leading zeroes in fractional seconds. */ 525 for(fracl = tzp - fracp - 1; fracl && fracp[fracl - 1] == '0'; fracl--) 526 ; ``` If `tzp == fracp`, then `fracl` is set to -1 in the loop initialization. I tested this on curl 8.9.0 commit `2a59c8d4cebfd199f930213ee82ae95f71e44578` (2024-07-24). I haven't looked when the issue was introduced. ## Steps To Reproduce: 1. Compile libcurl with `-fsanitize=address` and with gnutls. I used clang. `CC=clang CFLAGS=-fsanitize=address ../configure --disable-shared --enable-debug --with-gnutls=/usr/lib/aarch64-linux-gnu` 1. Compile the attached `poc.c` program which uses libcurl's `Curl_extract_certinfo`. 1. Run `./poc bad_cert_1.bin` The resulting report from AddressSanitizer: ``` ================================================================= ==2166==ERROR: AddressSanitizer: stack-buffer-overflow on address 0xffffaae02020 at pc 0xaaaad3fedb44 bp 0xffffee270350 sp 0xffffee26fb40 READ of size 4471 at 0xffffaae02020 thread T0 #0 0xaaaad3fedb40 in strlen (/root/work/curl/fuzz2/tests/unit/poc+0x11db40) (BuildId: 950d22dbc354c1f19b0a0459aa9b72f968a5aff4) #1 0xaaaad40dfb58 in formatf /root/work/curl/fuzz2/lib/../../lib/mprintf.c:883:15 #2 0xaaaad40e1f14 in Curl_dyn_vprintf /root/work/curl/fuzz2/lib/../../lib/mprintf.c:1105:9 #3 0xaaaad427c2ec in Curl_dyn_vaddf /root/work/curl/fuzz2/lib/../../lib/dynbuf.c:198:8 #4 0xaaaad427c844 in Curl_dyn_addf /root/work/curl/fuzz2/lib/../../lib/dynbuf.c:231:12 #5 0xaaaad41f0338 in GTime2str /root/work/curl/fuzz2/lib/../../lib/vtls/x509asn1.c:542:10 #6 0xaaaad41ec5fc in ASN1tostr /root/work/curl/fuzz2/lib/../../lib/vtls/x509asn1.c:632:14 #7 0xaaaad41eb410 in Curl …

Source code

lib/dynbuf.clines 223–234 · definition of Curl_dyn_addf
223CURLcode Curl_dyn_addf(struct dynbuf *s, const char *fmt, ...)
224{
225 CURLcode result;
226 va_list ap;
227 DEBUGASSERT(s);
228 DEBUGASSERT(s->init == DYNINIT);
229 DEBUGASSERT(!s->leng || s->bufr);
230 va_start(ap, fmt);
231 result = Curl_dyn_vaddf(s, fmt, ap);
C1: Curl_dyn_addf calls Curl_dyn_vaddf which leads to formatf/strlen
C4: Curl_dyn_addf -> Curl_dyn_vaddf -> formatf -> strlen
232 va_end(ap);
233 return result;
234}
lib/vtls/x509asn1.clines 484–548 · definition of GTime2str
484static CURLcode GTime2str(struct dynbuf *store,
485 const char *beg, const char *end)
486{
487 const char *tzp;
488 const char *fracp;
489 char sec1, sec2;
490 size_t fracl;
C1: fracl declared as size_t (unsigned)
491 size_t tzl;
492 const char *sep = "";
493
494 /* Convert an ASN.1 Generalized time to a printable string.
495 Return the dynamically allocated string, or NULL if an error occurs. */
496
497 for(fracp = beg; fracp < end && *fracp >= '0' && *fracp <= '9'; fracp++)
498 ;
499
500 /* Get seconds digits. */
501 sec1 = '0';
502 switch(fracp - beg - 12) {
503 case 0:
504 sec2 = '0';
505 break;
506 case 2:
507 sec1 = fracp[-2];
508 FALLTHROUGH();
509 case 1:
510 sec2 = fracp[-1];
511 break;
512 default:
513 return CURLE_BAD_FUNCTION_ARGUMENT;
514 }
515
516 /* Scan for timezone, measure fractional seconds. */
517 tzp = fracp;
C1: tzp = fracp; fracl = 0;
C3: tzp = fracp initially
518 fracl = 0;
519 if(fracp < end && (*fracp == '.' || *fracp == ',')) {
C1: if block for fractional part entered when '.' or ',' present
C3: if block entered for '.' or ','
520 fracp++;
C1: fracp++ advances past '.' or ','
C3: fracp++
521 do
C1: do-while advances tzp at least once
C3: do tzp++ while loop
522 tzp++;
523 while(tzp < end && *tzp >= '0' && *tzp <= '9');
524 /* Strip leading zeroes in fractional seconds. */
525 for(fracl = tzp - fracp - 1; fracl && fracp[fracl - 1] == '0'; fracl--)
C1: fracl = tzp - fracp - 1 becomes SIZE_MAX when tzp==fracp (no digits after indicator)
C2: for(fracl = tzp - fracp - 1; fracl && fracp[fracl - 1] == '0'; fracl--)
C3: fracl = tzp - fracp - 1
526 ;
527 }
528
529 /* Process timezone. */
530 if(tzp >= end)
531 ; /* Nothing to do. */
532 else if(*tzp == 'Z') {
533 tzp = " GMT";
534 end = tzp + 4;
535 }
536 else {
537 sep = " ";
538 tzp++;
539 }
540
541 tzl = end - tzp;
C4: tzl = end - tzp (not the source of the crash)
542 return Curl_dyn_addf(store,
C1: Curl_dyn_addf called with (int)fracl for %.*s precision
C4: Curl_dyn_addf with %.*s using (int)fracl and fracp
543 "%.4s-%.2s-%.2s %.2s:%.2s:%c%c%s%.*s%s%.*s",
544 beg, beg + 4, beg + 6,
545 beg + 8, beg + 10, sec1, sec2,
546 fracl? ".": "", (int)fracl, fracp,
547 sep, (int)tzl, tzp);
548}
lib/vtls/x509asn1.clines 1056–1216 · definition of Curl_extract_certinfo (truncated)
Show 161 lines (not cited by the judge)
1056CURLcode Curl_extract_certinfo(struct Curl_easy *data,
1057 int certnum,
1058 const char *beg,
1059 const char *end)
1060{
1061 struct Curl_X509certificate cert;
1062 struct Curl_asn1Element param;
1063 char *certptr;
1064 size_t clen;
1065 struct dynbuf out;
1066 CURLcode result = CURLE_OK;
1067 unsigned int version;
1068 const char *ptr;
1069 int rc;
1070
1071 if(!data->set.ssl.certinfo)
1072 if(certnum)
1073 return CURLE_OK;
1074
1075 Curl_dyn_init(&out, MAX_X509_STR);
1076 /* Prepare the certificate information for curl_easy_getinfo(). */
1077
1078 /* Extract the certificate ASN.1 elements. */
1079 if(Curl_parseX509(&cert, beg, end))
1080 return CURLE_PEER_FAILED_VERIFICATION;
1081
1082 /* Subject. */
1083 result = DNtostr(&out, &cert.subject);
1084 if(result)
1085 goto done;
1086 if(data->set.ssl.certinfo) {
1087 result = ssl_push_certinfo_dyn(data, certnum, "Subject", &out);
1088 if(result)
1089 goto done;
1090 }
1091 Curl_dyn_reset(&out);
1092
1093 /* Issuer. */
1094 result = DNtostr(&out, &cert.issuer);
1095 if(result)
1096 goto done;
1097 if(data->set.ssl.certinfo) {
1098 result = ssl_push_certinfo_dyn(data, certnum, "Issuer", &out);
1099 if(result)
1100 goto done;
1101 }
1102 Curl_dyn_reset(&out);
1103
1104 /* Version (always fits in less than 32 bits). */
1105 version = 0;
1106 for(ptr = cert.version.beg; ptr < cert.version.end; ptr++)
1107 version = (version << 8) | *(const unsigned char *) ptr;
1108 if(data->set.ssl.certinfo) {
1109 result = Curl_dyn_addf(&out, "%x", version);
1110 if(result)
1111 goto done;
1112 result = ssl_push_certinfo_dyn(data, certnum, "Version", &out);
1113 if(result)
1114 goto done;
1115 Curl_dyn_reset(&out);
1116 }
1117
1118 /* Serial number. */
1119 result = ASN1tostr(&out, &cert.serialNumber, 0);
1120 if(result)
1121 goto done;
1122 if(data->set.ssl.certinfo) {
1123 result = ssl_push_certinfo_dyn(data, certnum, "Serial Number", &out);
1124 if(result)
1125 goto done;
1126 }
1127 Curl_dyn_reset(&out);
1128
1129 /* Signature algorithm .*/
1130 result = dumpAlgo(&out, &param, cert.signatureAlgorithm.beg,
1131 cert.signatureAlgorithm.end);
1132 if(result)
1133 goto done;
1134 if(data->set.ssl.certinfo) {
1135 result = ssl_push_certinfo_dyn(data, certnum, "Signature Algorithm",
1136 &out);
1137 if(result)
1138 goto done;
1139 }
1140 Curl_dyn_reset(&out);
1141
1142 /* Start Date. */
1143 result = ASN1tostr(&out, &cert.notBefore, 0);
1144 if(result)
1145 goto done;
1146 if(data->set.ssl.certinfo) {
1147 result = ssl_push_certinfo_dyn(data, certnum, "Start Date", &out);
1148 if(result)
1149 goto done;
1150 }
1151 Curl_dyn_reset(&out);
1152
1153 /* Expire Date. */
1154 result = ASN1tostr(&out, &cert.notAfter, 0);
1155 if(result)
1156 goto done;
1157 if(data->set.ssl.certinfo) {
1158 result = ssl_push_certinfo_dyn(data, certnum, "Expire Date", &out);
1159 if(result)
1160 goto done;
1161 }
1162 Curl_dyn_reset(&out);
1163
1164 /* Public Key Algorithm. */
1165 result = dumpAlgo(&out, &param, cert.subjectPublicKeyAlgorithm.beg,
1166 cert.subjectPublicKeyAlgorithm.end);
1167 if(result)
1168 goto done;
1169 if(data->set.ssl.certinfo) {
1170 result = ssl_push_certinfo_dyn(data, certnum, "Public Key Algorithm",
1171 &out);
1172 if(result)
1173 goto done;
1174 }
1175
1176 rc = do_pubkey(data, certnum, Curl_dyn_ptr(&out),
1177 &param, &cert.subjectPublicKey);
1178 if(rc) {
1179 result = CURLE_OUT_OF_MEMORY; /* the most likely error */
1180 goto done;
1181 }
1182 Curl_dyn_reset(&out);
1183
1184 /* Signature. */
1185 result = ASN1tostr(&out, &cert.signature, 0);
1186 if(result)
1187 goto done;
1188 if(data->set.ssl.certinfo) {
1189 result = ssl_push_certinfo_dyn(data, certnum, "Signature", &out);
1190 if(result)
1191 goto done;
1192 }
1193 Curl_dyn_reset(&out);
1194
1195 /* Generate PEM certificate. */
1196 result = Curl_base64_encode(cert.certificate.beg,
1197 cert.certificate.end - cert.certificate.beg,
1198 &certptr, &clen);
1199 if(result)
1200 goto done;
1201
1202 /* Generate the final output certificate string. Format is:
1203 -----BEGIN CERTIFICATE-----\n
1204 <max 64 base64 characters>\n
1205 .
1206 .
1207 .
1208 -----END CERTIFICATE-----\n
1209 */
1210
1211 Curl_dyn_reset(&out);
1212
1213 /* Build the certificate string. */
1214 result = Curl_dyn_add(&out, "-----BEGIN CERTIFICATE-----\n");
1215 if(!result) {
1216 size_t j = 0;