hsts.c (15852B)
1 /*************************************************************************** 2 * _ _ ____ _ 3 * Project ___| | | | _ \| | 4 * / __| | | | |_) | | 5 * | (__| |_| | _ <| |___ 6 * \___|\___/|_| \_\_____| 7 * 8 * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al. 9 * 10 * This software is licensed as described in the file COPYING, which 11 * you should have received as part of this distribution. The terms 12 * are also available at https://curl.se/docs/copyright.html. 13 * 14 * You may opt to use, copy, modify, merge, publish, distribute and/or sell 15 * copies of the Software, and permit persons to whom the Software is 16 * furnished to do so, under the terms of the COPYING file. 17 * 18 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY 19 * KIND, either express or implied. 20 * 21 * SPDX-License-Identifier: curl 22 * 23 ***************************************************************************/ 24 /* 25 * The Strict-Transport-Security header is defined in RFC 6797: 26 * https://datatracker.ietf.org/doc/html/rfc6797 27 */ 28 #include "curl_setup.h" 29 30 #if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_HSTS) 31 #include <curl/curl.h> 32 #include "urldata.h" 33 #include "llist.h" 34 #include "hsts.h" 35 #include "curl_get_line.h" 36 #include "sendf.h" 37 #include "parsedate.h" 38 #include "fopen.h" 39 #include "rename.h" 40 #include "share.h" 41 #include "strdup.h" 42 #include "curlx/strparse.h" 43 44 /* The last 3 #include files should be in this order */ 45 #include "curl_printf.h" 46 #include "curl_memory.h" 47 #include "memdebug.h" 48 49 #define MAX_HSTS_LINE 4095 50 #define MAX_HSTS_HOSTLEN 2048 51 #define MAX_HSTS_DATELEN 256 52 #define UNLIMITED "unlimited" 53 54 #if defined(DEBUGBUILD) || defined(UNITTESTS) 55 /* to play well with debug builds, we can *set* a fixed time this will 56 return */ 57 time_t deltatime; /* allow for "adjustments" for unit test purposes */ 58 static time_t hsts_debugtime(void *unused) 59 { 60 const char *timestr = getenv("CURL_TIME"); 61 (void)unused; 62 if(timestr) { 63 curl_off_t val; 64 if(!curlx_str_number(×tr, &val, TIME_T_MAX)) 65 val += (curl_off_t)deltatime; 66 return (time_t)val; 67 } 68 return time(NULL); 69 } 70 #undef time 71 #define time(x) hsts_debugtime(x) 72 #endif 73 74 struct hsts *Curl_hsts_init(void) 75 { 76 struct hsts *h = calloc(1, sizeof(struct hsts)); 77 if(h) { 78 Curl_llist_init(&h->list, NULL); 79 } 80 return h; 81 } 82 83 static void hsts_free(struct stsentry *e) 84 { 85 free(CURL_UNCONST(e->host)); 86 free(e); 87 } 88 89 void Curl_hsts_cleanup(struct hsts **hp) 90 { 91 struct hsts *h = *hp; 92 if(h) { 93 struct Curl_llist_node *e; 94 struct Curl_llist_node *n; 95 for(e = Curl_llist_head(&h->list); e; e = n) { 96 struct stsentry *sts = Curl_node_elem(e); 97 n = Curl_node_next(e); 98 hsts_free(sts); 99 } 100 free(h->filename); 101 free(h); 102 *hp = NULL; 103 } 104 } 105 106 static CURLcode hsts_create(struct hsts *h, 107 const char *hostname, 108 size_t hlen, 109 bool subdomains, 110 curl_off_t expires) 111 { 112 DEBUGASSERT(h); 113 DEBUGASSERT(hostname); 114 115 if(hlen && (hostname[hlen - 1] == '.')) 116 /* strip off any trailing dot */ 117 --hlen; 118 if(hlen) { 119 char *duphost; 120 struct stsentry *sts = calloc(1, sizeof(struct stsentry)); 121 if(!sts) 122 return CURLE_OUT_OF_MEMORY; 123 124 duphost = Curl_memdup0(hostname, hlen); 125 if(!duphost) { 126 free(sts); 127 return CURLE_OUT_OF_MEMORY; 128 } 129 130 sts->host = duphost; 131 sts->expires = expires; 132 sts->includeSubDomains = subdomains; 133 Curl_llist_append(&h->list, sts, &sts->node); 134 } 135 return CURLE_OK; 136 } 137 138 CURLcode Curl_hsts_parse(struct hsts *h, const char *hostname, 139 const char *header) 140 { 141 const char *p = header; 142 curl_off_t expires = 0; 143 bool gotma = FALSE; 144 bool gotinc = FALSE; 145 bool subdomains = FALSE; 146 struct stsentry *sts; 147 time_t now = time(NULL); 148 size_t hlen = strlen(hostname); 149 150 if(Curl_host_is_ipnum(hostname)) 151 /* "explicit IP address identification of all forms is excluded." 152 / RFC 6797 */ 153 return CURLE_OK; 154 155 do { 156 curlx_str_passblanks(&p); 157 if(curl_strnequal("max-age", p, 7)) { 158 bool quoted = FALSE; 159 int rc; 160 161 if(gotma) 162 return CURLE_BAD_FUNCTION_ARGUMENT; 163 164 p += 7; 165 curlx_str_passblanks(&p); 166 if(curlx_str_single(&p, '=')) 167 return CURLE_BAD_FUNCTION_ARGUMENT; 168 curlx_str_passblanks(&p); 169 170 if(!curlx_str_single(&p, '\"')) 171 quoted = TRUE; 172 173 rc = curlx_str_number(&p, &expires, TIME_T_MAX); 174 if(rc == STRE_OVERFLOW) 175 expires = CURL_OFF_T_MAX; 176 else if(rc) 177 /* invalid max-age */ 178 return CURLE_BAD_FUNCTION_ARGUMENT; 179 180 if(quoted) { 181 if(*p != '\"') 182 return CURLE_BAD_FUNCTION_ARGUMENT; 183 p++; 184 } 185 gotma = TRUE; 186 } 187 else if(curl_strnequal("includesubdomains", p, 17)) { 188 if(gotinc) 189 return CURLE_BAD_FUNCTION_ARGUMENT; 190 subdomains = TRUE; 191 p += 17; 192 gotinc = TRUE; 193 } 194 else { 195 /* unknown directive, do a lame attempt to skip */ 196 while(*p && (*p != ';')) 197 p++; 198 } 199 200 curlx_str_passblanks(&p); 201 if(*p == ';') 202 p++; 203 } while(*p); 204 205 if(!gotma) 206 /* max-age is mandatory */ 207 return CURLE_BAD_FUNCTION_ARGUMENT; 208 209 if(!expires) { 210 /* remove the entry if present verbatim (without subdomain match) */ 211 sts = Curl_hsts(h, hostname, hlen, FALSE); 212 if(sts) { 213 Curl_node_remove(&sts->node); 214 hsts_free(sts); 215 } 216 return CURLE_OK; 217 } 218 219 if(CURL_OFF_T_MAX - now < expires) 220 /* would overflow, use maximum value */ 221 expires = CURL_OFF_T_MAX; 222 else 223 expires += now; 224 225 /* check if it already exists */ 226 sts = Curl_hsts(h, hostname, hlen, FALSE); 227 if(sts) { 228 /* just update these fields */ 229 sts->expires = expires; 230 sts->includeSubDomains = subdomains; 231 } 232 else 233 return hsts_create(h, hostname, hlen, subdomains, expires); 234 235 return CURLE_OK; 236 } 237 238 /* 239 * Return TRUE if the given hostname is currently an HSTS one. 240 * 241 * The 'subdomain' argument tells the function if subdomain matching should be 242 * attempted. 243 */ 244 struct stsentry *Curl_hsts(struct hsts *h, const char *hostname, 245 size_t hlen, bool subdomain) 246 { 247 struct stsentry *bestsub = NULL; 248 if(h) { 249 time_t now = time(NULL); 250 struct Curl_llist_node *e; 251 struct Curl_llist_node *n; 252 size_t blen = 0; 253 254 if((hlen > MAX_HSTS_HOSTLEN) || !hlen) 255 return NULL; 256 if(hostname[hlen-1] == '.') 257 /* remove the trailing dot */ 258 --hlen; 259 260 for(e = Curl_llist_head(&h->list); e; e = n) { 261 struct stsentry *sts = Curl_node_elem(e); 262 size_t ntail; 263 n = Curl_node_next(e); 264 if(sts->expires <= now) { 265 /* remove expired entries */ 266 Curl_node_remove(&sts->node); 267 hsts_free(sts); 268 continue; 269 } 270 ntail = strlen(sts->host); 271 if((subdomain && sts->includeSubDomains) && (ntail < hlen)) { 272 size_t offs = hlen - ntail; 273 if((hostname[offs-1] == '.') && 274 curl_strnequal(&hostname[offs], sts->host, ntail) && 275 (ntail > blen)) { 276 /* save the tail match with the longest tail */ 277 bestsub = sts; 278 blen = ntail; 279 } 280 } 281 /* avoid curl_strequal because the host name is not null-terminated */ 282 if((hlen == ntail) && curl_strnequal(hostname, sts->host, hlen)) 283 return sts; 284 } 285 } 286 return bestsub; 287 } 288 289 /* 290 * Send this HSTS entry to the write callback. 291 */ 292 static CURLcode hsts_push(struct Curl_easy *data, 293 struct curl_index *i, 294 struct stsentry *sts, 295 bool *stop) 296 { 297 struct curl_hstsentry e; 298 CURLSTScode sc; 299 struct tm stamp; 300 CURLcode result; 301 302 e.name = (char *)CURL_UNCONST(sts->host); 303 e.namelen = strlen(sts->host); 304 e.includeSubDomains = sts->includeSubDomains; 305 306 if(sts->expires != TIME_T_MAX) { 307 result = Curl_gmtime((time_t)sts->expires, &stamp); 308 if(result) 309 return result; 310 311 msnprintf(e.expire, sizeof(e.expire), "%d%02d%02d %02d:%02d:%02d", 312 stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday, 313 stamp.tm_hour, stamp.tm_min, stamp.tm_sec); 314 } 315 else 316 strcpy(e.expire, UNLIMITED); 317 318 sc = data->set.hsts_write(data, &e, i, 319 data->set.hsts_write_userp); 320 *stop = (sc != CURLSTS_OK); 321 return sc == CURLSTS_FAIL ? CURLE_BAD_FUNCTION_ARGUMENT : CURLE_OK; 322 } 323 324 /* 325 * Write this single hsts entry to a single output line 326 */ 327 static CURLcode hsts_out(struct stsentry *sts, FILE *fp) 328 { 329 struct tm stamp; 330 if(sts->expires != TIME_T_MAX) { 331 CURLcode result = Curl_gmtime((time_t)sts->expires, &stamp); 332 if(result) 333 return result; 334 fprintf(fp, "%s%s \"%d%02d%02d %02d:%02d:%02d\"\n", 335 sts->includeSubDomains ? ".": "", sts->host, 336 stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday, 337 stamp.tm_hour, stamp.tm_min, stamp.tm_sec); 338 } 339 else 340 fprintf(fp, "%s%s \"%s\"\n", 341 sts->includeSubDomains ? ".": "", sts->host, UNLIMITED); 342 return CURLE_OK; 343 } 344 345 346 /* 347 * Curl_https_save() writes the HSTS cache to file and callback. 348 */ 349 CURLcode Curl_hsts_save(struct Curl_easy *data, struct hsts *h, 350 const char *file) 351 { 352 struct Curl_llist_node *e; 353 struct Curl_llist_node *n; 354 CURLcode result = CURLE_OK; 355 FILE *out; 356 char *tempstore = NULL; 357 358 if(!h) 359 /* no cache activated */ 360 return CURLE_OK; 361 362 /* if no new name is given, use the one we stored from the load */ 363 if(!file && h->filename) 364 file = h->filename; 365 366 if((h->flags & CURLHSTS_READONLYFILE) || !file || !file[0]) 367 /* marked as read-only, no file or zero length filename */ 368 goto skipsave; 369 370 result = Curl_fopen(data, file, &out, &tempstore); 371 if(!result) { 372 fputs("# Your HSTS cache. https://curl.se/docs/hsts.html\n" 373 "# This file was generated by libcurl! Edit at your own risk.\n", 374 out); 375 for(e = Curl_llist_head(&h->list); e; e = n) { 376 struct stsentry *sts = Curl_node_elem(e); 377 n = Curl_node_next(e); 378 result = hsts_out(sts, out); 379 if(result) 380 break; 381 } 382 fclose(out); 383 if(!result && tempstore && Curl_rename(tempstore, file)) 384 result = CURLE_WRITE_ERROR; 385 386 if(result && tempstore) 387 unlink(tempstore); 388 } 389 free(tempstore); 390 skipsave: 391 if(data->set.hsts_write) { 392 /* if there is a write callback */ 393 struct curl_index i; /* count */ 394 i.total = Curl_llist_count(&h->list); 395 i.index = 0; 396 for(e = Curl_llist_head(&h->list); e; e = n) { 397 struct stsentry *sts = Curl_node_elem(e); 398 bool stop; 399 n = Curl_node_next(e); 400 result = hsts_push(data, &i, sts, &stop); 401 if(result || stop) 402 break; 403 i.index++; 404 } 405 } 406 return result; 407 } 408 409 /* only returns SERIOUS errors */ 410 static CURLcode hsts_add(struct hsts *h, const char *line) 411 { 412 /* Example lines: 413 example.com "20191231 10:00:00" 414 .example.net "20191231 10:00:00" 415 */ 416 struct Curl_str host; 417 struct Curl_str date; 418 419 if(curlx_str_word(&line, &host, MAX_HSTS_HOSTLEN) || 420 curlx_str_singlespace(&line) || 421 curlx_str_quotedword(&line, &date, MAX_HSTS_DATELEN) || 422 curlx_str_newline(&line)) 423 ; 424 else { 425 CURLcode result = CURLE_OK; 426 bool subdomain = FALSE; 427 struct stsentry *e; 428 char dbuf[MAX_HSTS_DATELEN + 1]; 429 time_t expires; 430 const char *hp = curlx_str(&host); 431 432 /* The date parser works on a null-terminated string. The maximum length 433 is upheld by curlx_str_quotedword(). */ 434 memcpy(dbuf, curlx_str(&date), curlx_strlen(&date)); 435 dbuf[curlx_strlen(&date)] = 0; 436 437 expires = strcmp(dbuf, UNLIMITED) ? Curl_getdate_capped(dbuf) : 438 TIME_T_MAX; 439 440 if(hp[0] == '.') { 441 curlx_str_nudge(&host, 1); 442 subdomain = TRUE; 443 } 444 /* only add it if not already present */ 445 e = Curl_hsts(h, curlx_str(&host), curlx_strlen(&host), subdomain); 446 if(!e) 447 result = hsts_create(h, curlx_str(&host), curlx_strlen(&host), 448 subdomain, expires); 449 else if(curlx_str_casecompare(&host, e->host)) { 450 /* the same hostname, use the largest expire time */ 451 if(expires > e->expires) 452 e->expires = expires; 453 } 454 if(result) 455 return result; 456 } 457 458 return CURLE_OK; 459 } 460 461 /* 462 * Load HSTS data from callback. 463 * 464 */ 465 static CURLcode hsts_pull(struct Curl_easy *data, struct hsts *h) 466 { 467 /* if the HSTS read callback is set, use it */ 468 if(data->set.hsts_read) { 469 CURLSTScode sc; 470 DEBUGASSERT(h); 471 do { 472 char buffer[MAX_HSTS_HOSTLEN + 1]; 473 struct curl_hstsentry e; 474 e.name = buffer; 475 e.namelen = sizeof(buffer)-1; 476 e.includeSubDomains = FALSE; /* default */ 477 e.expire[0] = 0; 478 e.name[0] = 0; /* just to make it clean */ 479 sc = data->set.hsts_read(data, &e, data->set.hsts_read_userp); 480 if(sc == CURLSTS_OK) { 481 time_t expires; 482 CURLcode result; 483 DEBUGASSERT(e.name[0]); 484 if(!e.name[0]) 485 /* bail out if no name was stored */ 486 return CURLE_BAD_FUNCTION_ARGUMENT; 487 if(e.expire[0]) 488 expires = Curl_getdate_capped(e.expire); 489 else 490 expires = TIME_T_MAX; /* the end of time */ 491 result = hsts_create(h, e.name, strlen(e.name), 492 /* bitfield to bool conversion: */ 493 e.includeSubDomains ? TRUE : FALSE, 494 expires); 495 if(result) 496 return result; 497 } 498 else if(sc == CURLSTS_FAIL) 499 return CURLE_ABORTED_BY_CALLBACK; 500 } while(sc == CURLSTS_OK); 501 } 502 return CURLE_OK; 503 } 504 505 /* 506 * Load the HSTS cache from the given file. The text based line-oriented file 507 * format is documented here: https://curl.se/docs/hsts.html 508 * 509 * This function only returns error on major problems that prevent hsts 510 * handling to work completely. It will ignore individual syntactical errors 511 * etc. 512 */ 513 static CURLcode hsts_load(struct hsts *h, const char *file) 514 { 515 CURLcode result = CURLE_OK; 516 FILE *fp; 517 518 /* we need a private copy of the filename so that the hsts cache file 519 name survives an easy handle reset */ 520 free(h->filename); 521 h->filename = strdup(file); 522 if(!h->filename) 523 return CURLE_OUT_OF_MEMORY; 524 525 fp = fopen(file, FOPEN_READTEXT); 526 if(fp) { 527 struct dynbuf buf; 528 curlx_dyn_init(&buf, MAX_HSTS_LINE); 529 while(Curl_get_line(&buf, fp)) { 530 const char *lineptr = curlx_dyn_ptr(&buf); 531 curlx_str_passblanks(&lineptr); 532 533 /* 534 * Skip empty or commented lines, since we know the line will have a 535 * trailing newline from Curl_get_line we can treat length 1 as empty. 536 */ 537 if((*lineptr == '#') || strlen(lineptr) <= 1) 538 continue; 539 540 hsts_add(h, lineptr); 541 } 542 curlx_dyn_free(&buf); /* free the line buffer */ 543 fclose(fp); 544 } 545 return result; 546 } 547 548 /* 549 * Curl_hsts_loadfile() loads HSTS from file 550 */ 551 CURLcode Curl_hsts_loadfile(struct Curl_easy *data, 552 struct hsts *h, const char *file) 553 { 554 DEBUGASSERT(h); 555 (void)data; 556 return hsts_load(h, file); 557 } 558 559 /* 560 * Curl_hsts_loadcb() loads HSTS from callback 561 */ 562 CURLcode Curl_hsts_loadcb(struct Curl_easy *data, struct hsts *h) 563 { 564 if(h) 565 return hsts_pull(data, h); 566 return CURLE_OK; 567 } 568 569 void Curl_hsts_loadfiles(struct Curl_easy *data) 570 { 571 struct curl_slist *l = data->state.hstslist; 572 if(l) { 573 Curl_share_lock(data, CURL_LOCK_DATA_HSTS, CURL_LOCK_ACCESS_SINGLE); 574 575 while(l) { 576 (void)Curl_hsts_loadfile(data, data->hsts, l->data); 577 l = l->next; 578 } 579 Curl_share_unlock(data, CURL_LOCK_DATA_HSTS); 580 } 581 } 582 583 #if defined(DEBUGBUILD) || defined(UNITTESTS) 584 #undef time 585 #endif 586 587 #endif /* CURL_DISABLE_HTTP || CURL_DISABLE_HSTS */