Coverage Report

Created: 2024-05-04 12:45

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