Coverage Report

Created: 2025-12-04 07:04

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