Coverage Report

Created: 2024-02-25 06:14

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