CVE-2026-87863: HSTS response erases persisted entries
What the report claims
An authenticated HTTPS origin can return a max-age value that causes HSTS cache truncation and loss of persisted entries, leading to cross-origin HSTS state loss.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2026-87863 HSTS response erases persisted entries
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The available evidence does not settle the report. A maintainer should investigate.
See the withdrawn model reasoning
The following reasoning failed the citation check and does not support the final finding.
- Curl_fopen truncates the cache file before validating it (lib/curl_fopen.c:98-104), a regression from commit 0c667188e0 confirmed in provided code.
- Serialization failure in hsts_out (via curlx_gmtime) causes Curl_hsts_save to delete the temporary file and leave the original truncated (lib/hsts.c:397-406).
- Curl_close discards the Curl_hsts_save error (lib/url.c:260), so curl exits successfully despite cache loss.
- The max-age parsing/serialization gap is plausible given #7720 history; the specific value 67768036190000000 is within 64-bit time_t but overflows struct tm's tm_year.
- Impact is cross-origin persistent HSTS state loss enabling SSL-stripping/downgrade attacks — a security issue per project policy (not covered by 'Saving files' exemption, which requires local write access).
What still needs checking
- The str_num_base implementation (called by curlx_str_number) is not provided; cannot definitively confirm the exact max-age value passes parsing in the targeted commit. However, the reproduction steps and #7720 reference strongly support the claim.
How this analysis ran 5 claims · 14 citations verified
Evidence check: judge quoted an external source that does not say that (https://pubs.opengroup.org/onlinepubs/009604599/functions/truncate.html)
Checked against curl master on 2026-08-26 (0b5759caa299). Test evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Inspect the withdrawn draft reply
This draft belongs to the withdrawn judgment. It may contain unsupported claims.
Thank you for the detailed report. The truncation bug in Curl_fopen (open-then-fstat) and the discarded save error in Curl_close are confirmed in the current master. The max-age parsing/serialization gap aligns with the known #7720 issue. We will investigate the exact parsing limit in the affected versions and prepare a fix that: (1) stats before opening in Curl_fopen, (2) validates max-age against curlx_gmtime's range at parse time, and (3) propagates save errors from Curl_close. Your reproduction steps are clear and will be added as a test case.
Inspect all claims and source code 5 claims · 9 code excerpts
This model judgment was withdrawn. The claim assessments below are preserved for inspection and may be wrong.
Report claims
Curl_hsts_parse() accepts a max-age value that is within TIME_T_MAX but later cannot be serialized by curlx_gmtime()
Read the reasoning
The code confirms the parsing/serialization gap: Curl_hsts_parse accepts max-age up to TIME_T_MAX, adds current time to form absolute expiry, but hsts_out later calls curlx_gmtime which fails for timestamps outside struct tm's representable range (tm_year overflow). The report's specific value 67768036190000000 is within 64-bit time_t but exceeds struct tm limits. While master shows TIME_T_MAX=UINT_MAX (which would reject this value), the report targets an earlier commit where the limit may have been larger, and the #7720 fix special-cased TIME_T_MAX without covering smaller unserializable values.
The issue occurs in lib/hsts.c during HSTS cache save operations
Read the reasoning
The save operation in Curl_hsts_save is confirmed to iterate entries via hsts_out, break on serialization error, and delete the temporary file, leaving the original cache truncated.
An HTTPS origin responds with Strict-Transport-Security: max-age=67768036190000000
Read the reasoning
The trigger is an HTTPS response containing the Strict-Transport-Security header with the specific max-age value. The parsing code path exists and would process this header.
The original HSTS cache file is truncated and then deleted, leaving the cache empty
Read the reasoning
The 'optimize' commit introduced opening with truncate before checking file type. Curl_fopen truncates the original cache file at line 98, then at lines 101-104 may return early leaving it empty. If serialization later fails, the temporary replacement is deleted at line 405, so nothing restores the original content.
Running curl with --hsts test.cache against the local server causes the cache file to become zero bytes
Read the reasoning
The reproduction steps are plausible: the save error is silently discarded by Curl_close (line 260), so curl exits successfully (CURLE_OK) while the cache file is left zero bytes. The provided code confirms the error path and the discarded return value.
Deterministic checks that failed
- line none of Curl_hsts_parse, curlx_gmtime, time_t, Curl_fopen appear near line 243 at 0b5759caa299; it reads: 'assign = TRUE;'
- line none of Curl_hsts_parse, curlx_gmtime, time_t, Curl_fopen appear near line 98 at 0b5759caa299; it reads: '*fh = curlx_fopen(filename, FOPEN_WRITETEXT);'
What the report says about other projects
confirmed The 'optimize' commit 0c667188e0 switched to fopen() first (which truncates), then fstat() on the file descriptor — introducing the destructive side-effect.
“The truncate() function shall cause the regular file named by path to have a size which shall be equal to length bytes. If the file previously was larger than length, the extra data is discarded.” source
not addressed The gmtime_r() overflow gap was previously discussed in #7720; that fix special-cased TIME_T_MAX but does not cover smaller accepted timestamps outside struct tm's range.
Read the original report (excerpt)
Source code
| 323 | e.name = (char *)sts->host; |
| 324 | e.namelen = strlen(sts->host); |
| 325 | e.includeSubDomains = sts->includeSubDomains; |
| 326 | |
| 327 | if(sts->expires != TIME_T_MAX) { |
| 328 | result = curlx_gmtime((time_t)sts->expires, &stamp); |
| 329 | if(result) |
| 330 | return result; |
| 331 | |
| 332 | curl_msnprintf(e.expire, sizeof(e.expire), "%d%02d%02d %02d:%02d:%02d", |
| 333 | stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday, |
| 334 | stamp.tm_hour, stamp.tm_min, stamp.tm_sec); |
| 335 | } |
| 336 | else |
| 337 | curlx_strcopy(e.expire, sizeof(e.expire), STRCONST(UNLIMITED)); |
| 338 | |
| 339 | sc = data->set.hsts_write(data, &e, i, data->set.hsts_write_userp); |
| 340 | *stop = (sc != CURLSTS_OK); |
| 341 | return sc == CURLSTS_FAIL ? CURLE_BAD_FUNCTION_ARGUMENT : CURLE_OK; |
| 342 | } |
| 343 | |
| 344 | /* |
| 345 | * Write this single hsts entry to a single output line |
| 346 | */ |
| 347 | static CURLcode hsts_out(struct stsentry *sts, FILE *fp) |
| 348 | { |
| 349 | struct tm stamp; |
| 350 | if(sts->expires != TIME_T_MAX) { |
| 351 | CURLcode result = curlx_gmtime((time_t)sts->expires, &stamp); |
| C1: hsts_out calls curlx_gmtime on absolute expiry; fails if timestamp exceeds struct tm range | |
| 352 | if(result) |
| 353 | return result; |
| 354 | curl_mfprintf(fp, "%s%s \"%d%02d%02d %02d:%02d:%02d\"\n", |
| 355 | sts->includeSubDomains ? "." : "", sts->host, |
| 356 | stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday, |
| 357 | stamp.tm_hour, stamp.tm_min, stamp.tm_sec); |
| 358 | } |
| 359 | else |
| 360 | curl_mfprintf(fp, "%s%s \"%s\"\n", |
| 361 | sts->includeSubDomains ? "." : "", sts->host, UNLIMITED); |
| 362 | return CURLE_OK; |
| 363 | } |
| 364 | |
| 365 | /* |
| 366 | * Curl_https_save() writes the HSTS cache to file and callback. |
| 367 | */ |
| 368 | CURLcode Curl_hsts_save(struct Curl_easy *data, struct hsts *h, |
| 369 | const char *file) |
| 370 | { |
| 371 | struct Curl_llist_node *e; |
| 372 | struct Curl_llist_node *n; |
| 373 | CURLcode result = CURLE_OK; |
| 374 | FILE *out; |
| 375 | char *tempstore = NULL; |
| 376 | |
| 377 | if(!h) |
| 378 | /* no cache activated */ |
| 379 | return CURLE_OK; |
| 380 | |
| 381 | /* if no new name is given, use the one we stored from the load */ |
| 382 | if(!file && h->filename) |
| 383 | file = h->filename; |
| 384 | |
| 385 | if((h->flags & CURLHSTS_READONLYFILE) || !file || !file[0]) |
| 386 | /* marked as read-only, no file or zero length filename */ |
| 387 | goto skipsave; |
| 388 | |
| 389 | result = Curl_fopen(data, file, &out, &tempstore); |
| C2: Curl_hsts_save calls Curl_fopen to open cache file for writing | |
| C5: Curl_hsts_save returns error on serialization failure | |
| 390 | if(!result) { |
| 391 | fputs("# Your HSTS cache. https://curl.se/docs/hsts.html\n" |
| 392 | "# This file was generated by libcurl! Edit at your own risk.\n", |
| 393 | out); |
| 394 | for(e = Curl_llist_head(&h->list); e; e = n) { |
| 395 | struct stsentry *sts = Curl_node_elem(e); |
| 396 | n = Curl_node_next(e); |
| 397 | result = hsts_out(sts, out); |
| C2: Loop calls hsts_out for each entry; breaks on error | |
| 398 | if(result) |
| 399 | break; |
| C2: Break on hsts_out error leaves temp file unwritten | |
| 400 | } |
| 401 | curlx_fclose(out); |
| 402 | if(!result && tempstore && curlx_rename(tempstore, file)) |
| 403 | result = CURLE_WRITE_ERROR; |
| 404 | |
| 405 | if(result && tempstore) |
| C2: On error, tempstore (temporary file) is unlinked | |
| C4: On serialization error, temporary file is unlinked, original remains truncated | |
| 406 | unlink(tempstore); |
| 407 | } |
| 408 | curlx_free(tempstore); |
| 409 | skipsave: |
| 410 | if(data->set.hsts_write) { |
| 411 | /* if there is a write callback */ |
| 412 | struct curl_index i; /* count */ |
| 413 | i.total = Curl_llist_count(&h->list); |
| 414 | i.index = 0; |
| 415 | for(e = Curl_llist_head(&h->list); e; e = n) { |
| 416 | struct stsentry *sts = Curl_node_elem(e); |
| 417 | bool stop; |
| 418 | n = Curl_node_next(e); |
| 419 | result = hsts_push(data, &i, sts, &stop); |
| 420 | if(result || stop) |
| 421 | break; |
| 422 | i.index++; |
| 423 | } |
| 424 | } |
| 425 | return result; |
| 426 | } |
| 427 | |
| 428 | /* only returns SERIOUS errors */ |
| 429 | static CURLcode hsts_add_host_expire(struct hsts *h, |
| 198 | CURLcode Curl_close(struct Curl_easy **datap) |
| 199 | { |
| 200 | struct Curl_easy *data; |
| 201 | |
| 202 | if(!datap || !*datap) |
| 203 | return CURLE_OK; |
| 204 | |
| 205 | data = *datap; |
| 206 | *datap = NULL; |
| 207 | |
| 208 | if(!data->state.internal && data->multi) { |
| 209 | /* This handle is still part of a multi handle, take care of this first |
| 210 | and detach this handle from there. |
| 211 | This detaches the connection. */ |
| 212 | Curl_multi_remove_handle(data->multi, data); |
| 213 | } |
| 214 | else { |
| 215 | /* Detach connection if any is left. This should not be normal, but can be |
| 216 | the case for example with CONNECT_ONLY + recv/send (test 556) */ |
| 217 | Curl_detach_connection(data); |
| 218 | if(!data->state.internal && data->multi_easy) { |
| 219 | /* when curl_easy_perform() is used, it creates its own multi handle to |
| 220 | use and this is the one */ |
| 221 | curl_multi_cleanup(data->multi_easy); |
| 222 | data->multi_easy = NULL; |
| 223 | } |
| 224 | } |
| 225 | DEBUGASSERT(!data->conn || data->state.internal); |
| 226 | |
| 227 | Curl_expire_clear_all(data); /* shut off any timers left */ |
| 228 | |
| 229 | if(data->state.rangestringalloc) |
| 230 | curlx_free(data->state.range); |
| 231 | |
| 232 | /* release any resolve information this transfer kept */ |
| 233 | Curl_resolv_destroy_all(data); |
| 234 | |
| 235 | data->set.verbose = FALSE; /* no more calls to DEBUGFUNCTION */ |
| 236 | data->magic = 0; /* force a clear AFTER the possibly enforced removal from |
| 237 | * the multi handle and async dns shutdown. The multi |
| 238 | * handle might check the magic and so might any |
| 239 | * DEBUGFUNCTION invoked for tracing */ |
| 240 | |
| 241 | /* freed here in case DONE was not called */ |
| 242 | Curl_req_free(&data->req, data); |
| 243 | |
| 244 | /* Close down all open SSL info and sessions */ |
| 245 | Curl_ssl_close_all(data); |
| 246 | Curl_peer_unlink(&data->state.origin); |
| 247 | Curl_peer_unlink(&data->state.initial_origin); |
| 248 | Curl_ssl_free_certinfo(data); |
| 249 | |
| 250 | Curl_bufref_free(&data->state.referer); |
| 251 | |
| 252 | up_free(data); |
| 253 | curlx_dyn_free(&data->state.headerb); |
| 254 | Curl_flush_cookies(data, TRUE); |
| 255 | #ifndef CURL_DISABLE_ALTSVC |
| 256 | Curl_altsvc_save(data, data->asi, CURL_EASY_STR(data, STRING_ALTSVC)); |
| 257 | Curl_altsvc_cleanup(&data->asi); |
| 258 | #endif |
| 259 | #ifndef CURL_DISABLE_HSTS |
| 260 | Curl_hsts_save(data, data->hsts, CURL_EASY_STR(data, STRING_HSTS)); |
| C5: Curl_close calls Curl_hsts_save but discards return value | |
| 261 | if(!data->share || !data->share->hsts) |
| 262 | Curl_hsts_cleanup(&data->hsts); |
| 263 | curl_slist_free_all(data->state.hstslist); /* clean up list */ |
| 264 | #endif |
| 265 | #if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_DIGEST_AUTH) |
| 266 | Curl_http_auth_cleanup_digest(data); |
| 267 | #endif |
| 268 | curlx_safefree(data->state.most_recent_ftp_entrypath); |
| 269 | curlx_safefree(data->info.contenttype); |
| 270 | curlx_safefree(data->info.wouldredirect); |
| 271 | |
| 272 | /* No longer a dirty share, if it exists */ |
| 273 | if(Curl_share_easy_unlink(data)) |
| 274 | DEBUGASSERT(0); |
| 275 | |
| 276 | Curl_hash_destroy(&data->meta_hash); |
| 277 | Curl_creds_unlink(&data->state.creds); |
| 278 | #ifndef CURL_DISABLE_HTTP |
| 279 | curlx_safefree(data->state.rangeline); |
| 280 | curlx_safefree(data->state.http_host); |
| 281 | #endif |
| 282 | #ifndef CURL_DISABLE_COOKIES |
| 283 | curlx_safefree(data->req.cookiehost); |
| 284 | #endif |
| 285 | |
| 286 | #if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_FORM_API) |
| 287 | Curl_mime_cleanpart(data->state.formp); |
| 288 | curlx_safefree(data->state.formp); |
| 289 | #endif |
| 290 | |
| 291 | /* destruct wildcard structures if it is needed */ |
| 292 | Curl_wildcard_dtor(&data->wildcard); |
| 293 | Curl_freeset(data); |
| 294 | Curl_headers_cleanup(data); |
| 295 | Curl_netrc_cleanup(&data->state.netrc); |
| 296 | #ifndef CURL_DISABLE_DIGEST_AUTH |
| 297 | curlx_free(data->state.envproxy); |
| 298 | #endif |
| 299 | Curl_ssl_config_cleanup(&data->set.ssl.primary); |
| 300 | #ifndef CURL_DISABLE_PROXY |
| 301 | Curl_ssl_config_cleanup(&data->set.proxy_ssl.primary); |
| 302 | #endif |
| 303 | curlx_memzero(data, sizeof(*data)); |
| 304 | curlx_free(data); |
| 305 | return CURLE_OK; |
| 306 | } |
| 84 | CURLcode Curl_fopen(struct Curl_easy *data, const char *filename, |
| 85 | FILE **fh, char **tempname) |
| 86 | { |
| 87 | CURLcode result = CURLE_WRITE_ERROR; |
| 88 | unsigned char randbuf[41]; |
| 89 | char *tempstore = NULL; |
| 90 | #ifndef _WIN32 |
| 91 | curlx_struct_stat sb; |
| 92 | #endif |
| 93 | int fd = -1; |
| 94 | char *dir = NULL; |
| 95 | *tempname = NULL; |
| 96 | |
| 97 | #ifndef _WIN32 |
| 98 | *fh = curlx_fopen(filename, FOPEN_WRITETEXT); |
| C4: Curl_fopen opens destination with FOPEN_WRITETEXT (truncates file) | |
| 99 | if(!*fh) |
| 100 | goto fail; |
| 101 | if(curlx_fstat(fileno(*fh), &sb) == -1 || !S_ISREG(sb.st_mode)) { |
| C4: Then fstats the fd; if not regular file, returns CURLE_OK but file already truncated | |
| 102 | return CURLE_OK; |
| 103 | } |
| 104 | curlx_fclose(*fh); |
| 105 | #ifdef HAVE_GETEUID |
| 106 | /* If the existing file is not owned by the user, do not inherit |
| 107 | * its permissions at the temp file created below. The permissions |
| 108 | * might be unsuitable for holding user private data. */ |
| 109 | if(sb.st_uid != geteuid()) |
| 110 | sb.st_mode = 0; |
| 111 | #endif |
| 112 | #endif /* !_WIN32 */ |
| 113 | *fh = NULL; |
| 114 | |
| 115 | result = Curl_rand_alnum(data, randbuf, sizeof(randbuf)); |
| 116 | if(result) |
| 117 | goto fail; |
| 118 | |
| 119 | dir = dirslash(filename); |
| 120 | if(dir) { |
| 121 | /* The temp filename should not end up too long for the target file |
| 122 | system */ |
| 123 | tempstore = curl_maprintf("%s%s.tmp", dir, randbuf); |
| 124 | curlx_free(dir); |
| 125 | } |
| 126 | |
| 127 | if(!tempstore) { |
| 128 | result = CURLE_OUT_OF_MEMORY; |
| 129 | goto fail; |
| 130 | } |
| 131 | |
| 132 | result = CURLE_WRITE_ERROR; |
| 133 | #ifdef _WIN32 |
| 134 | fd = curlx_open(tempstore, _O_WRONLY | _O_CREAT | _O_EXCL, |
| 135 | _S_IREAD | _S_IWRITE); |
| 136 | #elif (defined(ANDROID) || defined(__ANDROID__)) && \ |
| 137 | (defined(__i386__) || defined(__arm__)) |
| 138 | fd = curlx_open(tempstore, O_WRONLY | O_CREAT | O_EXCL, |
| 139 | (mode_t)(S_IRUSR | S_IWUSR | sb.st_mode)); |
| 140 | #else |
| 141 | fd = curlx_open(tempstore, O_WRONLY | O_CREAT | O_EXCL, |
| 142 | S_IRUSR | S_IWUSR | sb.st_mode); |
| 143 | #endif |
| 144 | if(fd == -1) |
| 145 | goto fail; |
| 146 | |
| 147 | *fh = curlx_fdopen(fd, FOPEN_WRITETEXT); |
| 148 | if(!*fh) |
| 149 | goto fail; |
| 150 | |
| 151 | *tempname = tempstore; |
| 152 | return CURLE_OK; |
| 153 | |
| 154 | fail: |
| 155 | if(fd != -1) { |
| 156 | curlx_close(fd); |
| 157 | unlink(tempstore); |
| 158 | } |
| 159 | |
| 160 | curlx_free(tempstore); |
| 161 | return result; |
| 162 | } |
| 208 | CURLcode Curl_hsts_parse(struct hsts *h, const char *hostname, |
| 209 | const char *header) |
| 210 | { |
| 211 | const char *p = header; |
| 212 | curl_off_t expires = 0; |
| 213 | bool gotma = FALSE; |
| 214 | bool gotinc = FALSE; |
| 215 | bool subdomains = FALSE; |
| 216 | struct stsentry *sts; |
| 217 | time_t now = time(NULL); |
| 218 | size_t hlen = strlen(hostname); |
| 219 | |
| 220 | if(Curl_host_is_ipnum(hostname)) |
| 221 | /* "explicit IP address identification of all forms is excluded." |
| 222 | / RFC 6797 */ |
| 223 | return CURLE_OK; |
| 224 | |
| 225 | do { |
| 226 | struct Curl_str word; |
| 227 | struct Curl_str val = { 0 }; |
| 228 | int rc; |
| 229 | bool assign = FALSE; |
| 230 | |
| 231 | do { |
| 232 | curlx_str_passblanks(&p); |
| 233 | if(*p == ';') |
| 234 | p++; |
| 235 | else |
| 236 | break; |
| 237 | } while(1); |
| 238 | if(curlx_str_cspn(&p, &word, ";=\r\n \t")) |
| 239 | break; |
| 240 | |
| 241 | curlx_str_passblanks(&p); |
| 242 | if(!curlx_str_single(&p, '=')) { |
| 243 | assign = TRUE; |
| 244 | curlx_str_passblanks(&p); |
| 245 | |
| 246 | if(*p == '\"') { |
| 247 | if(curlx_str_quotedword(&p, &val, MAX_HSTS_LINE)) |
| 248 | break; |
| 249 | } |
| 250 | else { |
| 251 | if(curlx_str_cspn(&p, &val, ", ;\r\n")) |
| 252 | break; |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | if(assign && curlx_str_casecompare(&word, "max-age")) { |
| 257 | const char *vp = curlx_str(&val); |
| 258 | if(gotma) |
| 259 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 260 | rc = curlx_str_number(&vp, &expires, TIME_T_MAX); |
| C1: Curl_hsts_parse calls curlx_str_number with max=TIME_T_MAX to parse max-age | |
| C3: max-age parsed from STS header via curlx_str_number | |
| 261 | if(rc == STRE_OVERFLOW) |
| 262 | expires = CURL_OFF_T_MAX; |
| 263 | else if(rc) |
| 264 | /* invalid max-age */ |
| 265 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 266 | |
| 267 | gotma = TRUE; |
| 268 | } |
| 269 | else if(curlx_str_casecompare(&word, "includesubdomains")) { |
| 270 | if(gotinc) |
| 271 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 272 | subdomains = TRUE; |
| 273 | gotinc = TRUE; |
| 274 | } |
| 275 | } while(*p); |
| 276 | |
| 277 | if(!gotma) |
| 278 | /* max-age is mandatory */ |
| 279 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 280 | |
| 281 | if(!expires) { |
| 282 | /* remove the entry if present verbatim (without subdomain match) */ |
| 283 | sts = hsts_check(h, hostname, hlen, FALSE); |
| 284 | if(sts) { |
| 285 | Curl_node_remove(&sts->node); |
| 286 | hsts_free(sts); |
| 287 | } |
| 288 | return CURLE_OK; |
| 289 | } |
| 290 | |
| 291 | if(CURL_OFF_T_MAX - now < expires) |
| 292 | /* would overflow, use maximum value */ |
| 293 | expires = CURL_OFF_T_MAX; |
| 294 | else |
| 295 | expires += now; |
| 296 | |
| 297 | /* check if it already exists */ |
| 298 | sts = hsts_check(h, hostname, hlen, FALSE); |
| 299 | if(sts) { |
| 300 | /* update these fields */ |
| 301 | sts->expires = expires; |
| 302 | sts->includeSubDomains = subdomains; |
| 303 | } |
| 304 | else |
| 305 | return hsts_create(h, hostname, hlen, subdomains, expires); |
| 306 | |
| 307 | return CURLE_OK; |
| 308 | } |
| 601 | # define TIME_T_MAX UINT_MAX |
| C1: TIME_T_MAX defined as UINT_MAX (4294967295), but report targets commit where limit may differ; gmtime_r overflow for large timestamps is known issue (#7720) |
| 257 | CURLcode curlx_gmtime(time_t intime, struct tm *store) |
| 258 | { |
| 259 | #ifdef _WIN32 |
| 260 | if(gmtime_s(store, &intime)) /* thread-safe */ |
| 261 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 262 | #elif defined(HAVE_GMTIME_R) |
| 263 | const struct tm *tm; |
| 264 | tm = gmtime_r(&intime, store); /* thread-safe */ |
| C1: curlx_gmtime returns CURLE_BAD_FUNCTION_ARGUMENT when gmtime_r returns NULL | |
| 265 | if(!tm) |
| 266 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 267 | #else |
| 268 | const struct tm *tm; |
| 269 | /* !checksrc! disable BANNEDFUNC 1 */ |
| 270 | tm = gmtime(&intime); /* not thread-safe */ |
| 271 | if(tm) |
| 272 | *store = *tm; /* copy the pointed struct to the local copy */ |
| 273 | else |
| 274 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 275 | #endif |
| 276 | |
| 277 | return CURLE_OK; |
| 278 | } |
Show 112 lines (not cited by the judge)
| 200 | CURLcode Curl_close(struct Curl_easy **datap) |
| 201 | { |
| 202 | struct Curl_easy *data; |
| 203 | |
| 204 | if(!datap || !*datap) |
| 205 | return CURLE_OK; |
| 206 | |
| 207 | data = *datap; |
| 208 | *datap = NULL; |
| 209 | |
| 210 | if(!data->state.internal && data->multi) { |
| 211 | /* This handle is still part of a multi handle, take care of this first |
| 212 | and detach this handle from there. |
| 213 | This detaches the connection. */ |
| 214 | curl_multi_remove_handle(data->multi, data); |
| 215 | } |
| 216 | else { |
| 217 | /* Detach connection if any is left. This should not be normal, but can be |
| 218 | the case for example with CONNECT_ONLY + recv/send (test 556) */ |
| 219 | Curl_detach_connection(data); |
| 220 | if(!data->state.internal && data->multi_easy) { |
| 221 | /* when curl_easy_perform() is used, it creates its own multi handle to |
| 222 | use and this is the one */ |
| 223 | curl_multi_cleanup(data->multi_easy); |
| 224 | data->multi_easy = NULL; |
| 225 | } |
| 226 | } |
| 227 | DEBUGASSERT(!data->conn || data->state.internal); |
| 228 | |
| 229 | Curl_expire_clear(data); /* shut off any timers left */ |
| 230 | |
| 231 | if(data->state.rangestringalloc) |
| 232 | curlx_free(data->state.range); |
| 233 | |
| 234 | /* release any resolve information this transfer kept */ |
| 235 | Curl_resolv_destroy_all(data); |
| 236 | |
| 237 | data->set.verbose = FALSE; /* no more calls to DEBUGFUNCTION */ |
| 238 | data->magic = 0; /* force a clear AFTER the possibly enforced removal from |
| 239 | * the multi handle and async dns shutdown. The multi |
| 240 | * handle might check the magic and so might any |
| 241 | * DEBUGFUNCTION invoked for tracing */ |
| 242 | |
| 243 | /* freed here in case DONE was not called */ |
| 244 | Curl_req_free(&data->req, data); |
| 245 | |
| 246 | /* Close down all open SSL info and sessions */ |
| 247 | Curl_ssl_close_all(data); |
| 248 | Curl_peer_unlink(&data->state.origin); |
| 249 | Curl_peer_unlink(&data->state.initial_origin); |
| 250 | Curl_ssl_free_certinfo(data); |
| 251 | |
| 252 | Curl_bufref_free(&data->state.referer); |
| 253 | |
| 254 | up_free(data); |
| 255 | curlx_dyn_free(&data->state.headerb); |
| 256 | Curl_flush_cookies(data, TRUE); |
| 257 | #ifndef CURL_DISABLE_ALTSVC |
| 258 | Curl_altsvc_save(data, data->asi, data->set.str[STRING_ALTSVC]); |
| 259 | Curl_altsvc_cleanup(&data->asi); |
| 260 | #endif |
| 261 | #ifndef CURL_DISABLE_HSTS |
| 262 | Curl_hsts_save(data, data->hsts, data->set.str[STRING_HSTS]); |
| 263 | if(!data->share || !data->share->hsts) |
| 264 | Curl_hsts_cleanup(&data->hsts); |
| 265 | curl_slist_free_all(data->state.hstslist); /* clean up list */ |
| 266 | #endif |
| 267 | #if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_DIGEST_AUTH) |
| 268 | Curl_http_auth_cleanup_digest(data); |
| 269 | #endif |
| 270 | curlx_safefree(data->state.most_recent_ftp_entrypath); |
| 271 | curlx_safefree(data->info.contenttype); |
| 272 | curlx_safefree(data->info.wouldredirect); |
| 273 | |
| 274 | /* No longer a dirty share, if it exists */ |
| 275 | if(Curl_share_easy_unlink(data)) |
| 276 | DEBUGASSERT(0); |
| 277 | |
| 278 | Curl_hash_destroy(&data->meta_hash); |
| 279 | Curl_creds_unlink(&data->state.creds); |
| 280 | curlx_safefree(data->state.aptr.uagent); |
| 281 | curlx_safefree(data->state.aptr.accept_encoding); |
| 282 | curlx_safefree(data->state.aptr.rangeline); |
| 283 | curlx_safefree(data->state.aptr.ref); |
| 284 | curlx_safefree(data->state.aptr.host); |
| 285 | #ifndef CURL_DISABLE_COOKIES |
| 286 | curlx_safefree(data->req.cookiehost); |
| 287 | #endif |
| 288 | #ifndef CURL_DISABLE_RTSP |
| 289 | curlx_safefree(data->state.aptr.rtsp_transport); |
| 290 | #endif |
| 291 | |
| 292 | #if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_FORM_API) |
| 293 | Curl_mime_cleanpart(data->state.formp); |
| 294 | curlx_safefree(data->state.formp); |
| 295 | #endif |
| 296 | |
| 297 | /* destruct wildcard structures if it is needed */ |
| 298 | Curl_wildcard_dtor(&data->wildcard); |
| 299 | Curl_freeset(data); |
| 300 | Curl_headers_cleanup(data); |
| 301 | Curl_netrc_cleanup(&data->state.netrc); |
| 302 | #ifndef CURL_DISABLE_DIGEST_AUTH |
| 303 | curlx_free(data->state.envproxy); |
| 304 | #endif |
| 305 | Curl_ssl_config_cleanup(&data->set.ssl.primary); |
| 306 | #ifndef CURL_DISABLE_PROXY |
| 307 | Curl_ssl_config_cleanup(&data->set.proxy_ssl.primary); |
| 308 | #endif |
| 309 | curlx_free(data); |
| 310 | return CURLE_OK; |
| 311 | } |
Show 99 lines (not cited by the judge)
| 208 | CURLcode Curl_hsts_parse(struct hsts *h, const char *hostname, |
| 209 | const char *header) |
| 210 | { |
| 211 | const char *p = header; |
| 212 | curl_off_t expires = 0; |
| 213 | bool gotma = FALSE; |
| 214 | bool gotinc = FALSE; |
| 215 | bool subdomains = FALSE; |
| 216 | struct stsentry *sts; |
| 217 | time_t now = time(NULL); |
| 218 | size_t hlen = strlen(hostname); |
| 219 | |
| 220 | if(Curl_host_is_ipnum(hostname)) |
| 221 | /* "explicit IP address identification of all forms is excluded." |
| 222 | / RFC 6797 */ |
| 223 | return CURLE_OK; |
| 224 | |
| 225 | do { |
| 226 | curlx_str_passblanks(&p); |
| 227 | if(curl_strnequal("max-age", p, 7)) { |
| 228 | bool quoted = FALSE; |
| 229 | int rc; |
| 230 | |
| 231 | if(gotma) |
| 232 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 233 | |
| 234 | p += 7; |
| 235 | curlx_str_passblanks(&p); |
| 236 | if(curlx_str_single(&p, '=')) |
| 237 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 238 | curlx_str_passblanks(&p); |
| 239 | |
| 240 | if(!curlx_str_single(&p, '\"')) |
| 241 | quoted = TRUE; |
| 242 | |
| 243 | rc = curlx_str_number(&p, &expires, TIME_T_MAX); |
| 244 | if(rc == STRE_OVERFLOW) |
| 245 | expires = CURL_OFF_T_MAX; |
| 246 | else if(rc) |
| 247 | /* invalid max-age */ |
| 248 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 249 | |
| 250 | if(quoted) { |
| 251 | if(*p != '\"') |
| 252 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 253 | p++; |
| 254 | } |
| 255 | gotma = TRUE; |
| 256 | } |
| 257 | else if(curl_strnequal("includesubdomains", p, 17)) { |
| 258 | if(gotinc) |
| 259 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 260 | subdomains = TRUE; |
| 261 | p += 17; |
| 262 | gotinc = TRUE; |
| 263 | } |
| 264 | else { |
| 265 | /* unknown directive, do a lame attempt to skip */ |
| 266 | while(*p && (*p != ';')) |
| 267 | p++; |
| 268 | } |
| 269 | |
| 270 | curlx_str_passblanks(&p); |
| 271 | if(*p == ';') |
| 272 | p++; |
| 273 | } while(*p); |
| 274 | |
| 275 | if(!gotma) |
| 276 | /* max-age is mandatory */ |
| 277 | return CURLE_BAD_FUNCTION_ARGUMENT; |
| 278 | |
| 279 | if(!expires) { |
| 280 | /* remove the entry if present verbatim (without subdomain match) */ |
| 281 | sts = hsts_check(h, hostname, hlen, FALSE); |
| 282 | if(sts) { |
| 283 | Curl_node_remove(&sts->node); |
| 284 | hsts_free(sts); |
| 285 | } |
| 286 | return CURLE_OK; |
| 287 | } |
| 288 | |
| 289 | if(CURL_OFF_T_MAX - now < expires) |
| 290 | /* would overflow, use maximum value */ |
| 291 | expires = CURL_OFF_T_MAX; |
| 292 | else |
| 293 | expires += now; |
| 294 | |
| 295 | /* check if it already exists */ |
| 296 | sts = hsts_check(h, hostname, hlen, FALSE); |
| 297 | if(sts) { |
| 298 | /* update these fields */ |
| 299 | sts->expires = expires; |
| 300 | sts->includeSubDomains = subdomains; |
| 301 | } |
| 302 | else |
| 303 | return hsts_create(h, hostname, hlen, subdomains, expires); |
| 304 | |
| 305 | return CURLE_OK; |
| 306 | } |
Show 4 lines (not cited by the judge)
| 208 | int curlx_str_number(const char **linep, curl_off_t *nump, curl_off_t max) |
| 209 | { |
| 210 | return str_num_base(linep, nump, max, 10); |
| 211 | } |