✓ProveIt
Examples / Report #1897203 Saved analysis

CVE-2023-27537: HSTS double-free

curl · 2023-03-0819.6s analysis · $0.019 in model calls

What the report claims

Double-free or use-after-free may occur in libcurl's HSTS handling when multiple threads process HSTS entries concurrently, leading to memory corruption.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2023-27537 HSTS double free

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

ProveIt's finding

Needs human review

The available evidence does not settle the report. A maintainer should investigate.

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.

  • Curl_hsts_parse (lines 213-221) and Curl_hsts (lines 270-275) remove and free HSTS entries without acquiring the share lock (CURL_LOCK_DATA_HSTS) that applications register via CURLOPT_SHARE.
  • The share mechanism expects curl to call the lock/unlock callbacks when accessing shared data; the HSTS code does not, so concurrent threads race on list removal and free.
  • Double-free and use-after-free are exploitable heap corruption bugs; a malicious server can trigger the max-age=0 or expiry path while other threads traverse the list.
  • The provided code snippets show no locking in the HSTS implementation. The deterministic checks confirm the quoted lines exist at the target revision.
How this analysis ran 6 claims · 19 citations verified

Evidence check: judge quoted an external source that does not say that (https://datatracker.ietf.org/doc/html/draft-ietf-websec-key-pinning-13)

Checked against curl master on 2023-03-08 (b1d735956f42). Dev 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 · 10 references checked out, 1 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
6 claims · 2194 tokens · $0.0002
3 · Evidence
4 code snippets
real curl source at b1d735956f42 · 3 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
10658 tokens · 17.1s · $0.0184
5 · Receipts
19 of 19 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 in lib/hsts.c (Curl_hsts_parse lines 213-221 and Curl_hsts lines 270-275) removes and frees HSTS entries without acquiring the share lock (CURL_LOCK_DATA_HSTS), so concurrent threads sharing an HSTS cache can double-free or use-after-free. This is a valid thread-safety bug in the HSTS implementation when used with CURLOPT_SHARE. We will investigate the proper locking integration and prepare a fix.

Inspect all claims and source code 6 claims · 4 code excerpts

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

Report claims

C1flaw supported

Double-free may occur when multiple threads call Curl_hsts_free on the same HSTS entry simultaneously

Read the reasoning

The code in Curl_hsts_parse (lines 213-221) and Curl_hsts (lines 270-275) removes entries from the shared list and frees them via hsts_free without acquiring any lock. When multiple threads share an HSTS cache via CURLOPT_SHARE with CURL_LOCK_DATA_HSTS, the lock callbacks are never invoked by this code, allowing concurrent free of the same entry.

C2flaw supported

Use-after-free may occur when other threads access HSTS entries after they have been freed

Read the reasoning

Curl_hsts traverses the list (line 267) and accesses entry fields (lines 270, 276-282) without locking. Another thread could have removed and freed the same entry via the max-age=0 or expiry path, leading to use-after-free.

C3flaw supported

The function Curl_hsts_parse removes HSTS entries without proper synchronization

Read the reasoning

Curl_hsts_parse removes entries when max-age=0 (lines 213-221) without any synchronization primitive. The share mechanism's lock callbacks are not used.

C4flaw supported

The removal of HSTS entries occurs without exclusivity control in multi-threaded context

Read the reasoning

The term 'exclusivity control' maps to the share lock (CURL_LOCK_DATA_HSTS). The code performs lookup, removal, and free without acquiring that lock, so concurrent threads have no exclusion.

C5flaw supported

The code removes entries verbatim without subdomain matching when max-age=0 is received

Read the reasoning

Line 215 passes FALSE as the third argument to Curl_hsts, disabling subdomain matching for the verbatim removal when max-age=0.

C6flaw supported

Lines 270-275 in lib/hsts.c have a similar concurrency issue

Read the reasoning

Lines 270-275 in the provided source (Curl_hsts function) show the same pattern: expired entries are removed and freed without locking, creating an identical race condition.

Deterministic checks that failed

  • quote only 0/4 quoted lines appear anywhere in curl at b1d735956f42

What the report says about other projects

confirmed HSTS entries disappear when they expire or when "max-age=0" is received

“When a UA connects to a Known Expect-CT Host using a TLS connection, if the TLS connection has errors, the UA MUST terminate the connection without allowing the user to proceed anyway. (This behavior is the same as that required by [RFC6797].) If the connection has no errors, then the UA will apply an additional correctness check: compliance with a CT Policy. A UA should evaluate compliance with its CT Policy whenever connecting to a Known Expect-CT Host, as soon as possible. It is acceptable to skip this CT compliance check for some hosts according to local policy. For example, a UA may disable CT compliance checks for hosts whose validated certificate chain terminates at a user-defined trust anchor, rather than a trust anchor built-in to the UA (or underlying platform). To perform Pin Validation, the UA will compute the SPKI Fingerprints for each certificate in the Pinned Host's” source

Read the original report (excerpt)

Full report on HackerOne ↗

## Summary: When processing HSTS with multi-threading, double-free or UAF may occur due to lack of exclusion control. HSTS entries disappear when they expire or when "max-age=0" is received. In this case, the offending entry is removed from the internal memory list, freeing memory but not exclusivity control. Therefore, depending on the timing, other threads may perform the operation, resulting in double-free or UAF. `lib/hsts.c` in the function `Curl_hsts_parse` on lines 213-221 ``` if(!expires) { /* remove the entry if present verbatim (without subdomain match) */ sts = Curl_hsts(h, hostname, FALSE); if(sts) { Curl_llist_remove(&h->list, &sts->node, NULL); hsts_free(sts); } return CURLE_OK; } ``` If multiple threads process `hsts_free(sts);` at the same time, it becomes double-free. Another problem is that UAF occurs when other threads access entries. Lines 270-275 have a similar problem. ## Steps To Reproduce: 1. [Prepare the following php.] ``` <?php $random = rand(0, 1); if($random == 0){ header("strict-transport-security: max-age=9999"); }else{ header("strict-transport-security: max-age=0"); } ``` 2. [Compile and run the following cpp.] ``` #include <stdio.h> #define HAVE_STRUCT_TIMESPEC // [Add] #include <pthread.h> #include <curl/curl.h> #define NUMT 100 const char* const url = "https://test.local/poc.php"; pthread_mutex_t lock[9]; static void lock_cb(CURL* handle, curl_lock_data data, curl_lock_access access, void* userptr) { pthread_mutex_lock(&lock[data]); /* uses a global lock array */ } static void unlock_cb(CURL* handle, curl_lock_data data, void* userptr) { pthread_mutex_unlock(&lock[data]); /* uses a global lock array */ } static void* pull_one_url(void* shobject) { CURL* curl; for (int i = 0; i < 100; i++) { curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_HSTS, "c:\\home\\hsts.txt"); curl_easy_setopt(curl, CURLOPT_SHARE, shobject); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_perform(curl); /* ignores error */ curl_easy_cleanup(curl); } return NULL; } int main(int argc, char** argv) { pthread_t tid[NUMT] = {0}; int i; for(i = 0;i<=9;i++) pt …

Source code

lib/hsts.clines 142–246 · lines 213-221 cited by the report; definition of Curl_hsts_parse
142CURLcode Curl_hsts_parse(struct hsts *h, const char *hostname,
143 const char *header)
144{
145 const char *p = header;
146 curl_off_t expires = 0;
147 bool gotma = FALSE;
148 bool gotinc = FALSE;
149 bool subdomains = FALSE;
150 struct stsentry *sts;
151 time_t now = time(NULL);
152
153 if(Curl_host_is_ipnum(hostname))
154 /* "explicit IP address identification of all forms is excluded."
155 / RFC 6797 */
156 return CURLE_OK;
157
158 do {
159 while(*p && ISBLANK(*p))
160 p++;
161 if(strncasecompare("max-age=", p, 8)) {
162 bool quoted = FALSE;
163 CURLofft offt;
164 char *endp;
165
166 if(gotma)
167 return CURLE_BAD_FUNCTION_ARGUMENT;
168
169 p += 8;
170 while(*p && ISBLANK(*p))
171 p++;
172 if(*p == '\"') {
173 p++;
174 quoted = TRUE;
175 }
176 offt = curlx_strtoofft(p, &endp, 10, &expires);
177 if(offt == CURL_OFFT_FLOW)
178 expires = CURL_OFF_T_MAX;
179 else if(offt)
180 /* invalid max-age */
181 return CURLE_BAD_FUNCTION_ARGUMENT;
182 p = endp;
183 if(quoted) {
184 if(*p != '\"')
185 return CURLE_BAD_FUNCTION_ARGUMENT;
186 p++;
187 }
188 gotma = TRUE;
189 }
190 else if(strncasecompare("includesubdomains", p, 17)) {
191 if(gotinc)
192 return CURLE_BAD_FUNCTION_ARGUMENT;
193 subdomains = TRUE;
194 p += 17;
195 gotinc = TRUE;
196 }
197 else {
198 /* unknown directive, do a lame attempt to skip */
199 while(*p && (*p != ';'))
200 p++;
201 }
202
203 while(*p && ISBLANK(*p))
204 p++;
205 if(*p == ';')
206 p++;
207 } while (*p);
208
209 if(!gotma)
210 /* max-age is mandatory */
211 return CURLE_BAD_FUNCTION_ARGUMENT;
212
213 if(!expires) {
C3: Entry removal path starts here
214 /* remove the entry if present verbatim (without subdomain match) */
215 sts = Curl_hsts(h, hostname, FALSE);
C4: Curl_hsts lookup without lock
C5: Curl_hsts called with FALSE for subdomain parameter
216 if(sts) {
217 Curl_llist_remove(&h->list, &sts->node, NULL);
C1: Curl_llist_remove called without locking
C3: Curl_llist_remove without lock
C4: List removal without lock
218 hsts_free(sts);
C1: hsts_free called immediately after removal, no synchronization visible
C3: hsts_free without lock
C4: Free without lock
219 }
220 return CURLE_OK;
221 }
222
223 if(CURL_OFF_T_MAX - now < expires)
224 /* would overflow, use maximum value */
225 expires = CURL_OFF_T_MAX;
226 else
227 expires += now;
228
229 /* check if it already exists */
230 sts = Curl_hsts(h, hostname, FALSE);
231 if(sts) {
232 /* just update these fields */
233 sts->expires = expires;
234 sts->includeSubDomains = subdomains;
235 }
236 else
237 return hsts_create(h, hostname, subdomains, expires);
238
239 return CURLE_OK;
240}
241
242/*
243 * Return TRUE if the given host name is currently an HSTS one.
244 *
245 * The 'subdomain' argument tells the function if subdomain matching should be
246 * attempted.
lib/hsts.clines 248–290 · definition of Curl_hsts
248struct stsentry *Curl_hsts(struct hsts *h, const char *hostname,
249 bool subdomain)
C5: Curl_hsts signature shows subdomain parameter
250{
251 if(h) {
252 char buffer[MAX_HSTS_HOSTLEN + 1];
253 time_t now = time(NULL);
254 size_t hlen = strlen(hostname);
255 struct Curl_llist_element *e;
256 struct Curl_llist_element *n;
257
258 if((hlen > MAX_HSTS_HOSTLEN) || !hlen)
259 return NULL;
260 memcpy(buffer, hostname, hlen);
261 if(hostname[hlen-1] == '.')
262 /* remove the trailing dot */
263 --hlen;
264 buffer[hlen] = 0;
265 hostname = buffer;
266
267 for(e = h->list.head; e; e = n) {
C2: Curl_hsts iterates list without locking
268 struct stsentry *sts = e->ptr;
269 n = e->next;
270 if(sts->expires <= now) {
C2: Accesses sts->expires after potential concurrent free
C6: Expired entry check
271 /* remove expired entries */
272 Curl_llist_remove(&h->list, &sts->node, NULL);
C1: Same pattern in Curl_hsts for expired entries
C6: Curl_llist_remove without lock
273 hsts_free(sts);
C1: hsts_free called without locking
C6: hsts_free without lock
274 continue;
275 }
276 if(subdomain && sts->includeSubDomains) {
C2: Accesses sts->includeSubDomains and sts->host after potential concurrent free
277 size_t ntail = strlen(sts->host);
278 if(ntail < hlen) {
279 size_t offs = hlen - ntail;
280 if((hostname[offs-1] == '.') &&
281 strncasecompare(&hostname[offs], sts->host, ntail))
282 return sts;
283 }
284 }
285 if(strcasecompare(hostname, sts->host))
286 return sts;
287 }
288 }
289 return NULL; /* no match */
290}
lib/hsts.clines 85–89 · definition of hsts_free
85static void hsts_free(struct stsentry *e)
86{
87 free((char *)e->host);
C1: hsts_free frees host and entry memory
88 free(e);
89}
lib/llist.clines 94–131 · definition of Curl_llist_remove
Show 38 lines (not cited by the judge)
94void
95Curl_llist_remove(struct Curl_llist *list, struct Curl_llist_element *e,
96 void *user)
97{
98 void *ptr;
99 if(!e || list->size == 0)
100 return;
101
102 if(e == list->head) {
103 list->head = e->next;
104
105 if(!list->head)
106 list->tail = NULL;
107 else
108 e->next->prev = NULL;
109 }
110 else {
111 if(e->prev)
112 e->prev->next = e->next;
113
114 if(!e->next)
115 list->tail = e->prev;
116 else
117 e->next->prev = e->prev;
118 }
119
120 ptr = e->ptr;
121
122 e->ptr = NULL;
123 e->prev = NULL;
124 e->next = NULL;
125
126 --list->size;
127
128 /* call the dtor() last for when it actually frees the 'e' memory itself */
129 if(list->dtor)
130 list->dtor(user, ptr);
131}