Coverage Report

Created: 2025-12-31 07:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wget2/libwget/hsts.c
Line
Count
Source
1
/*
2
 * Copyright (c) 2014 Tim Ruehsen
3
 * Copyright (c) 2015-2024 Free Software Foundation, Inc.
4
 *
5
 * This file is part of libwget.
6
 *
7
 * Libwget is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Lesser General Public License as published by
9
 * the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * Libwget is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Lesser General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Lesser General Public License
18
 * along with libwget.  If not, see <https://www.gnu.org/licenses/>.
19
 *
20
 *
21
 * HSTS routines
22
 *
23
 * Changelog
24
 * 28.01.2014  Tim Ruehsen  created
25
 *
26
 */
27
28
#include <config.h>
29
30
#include <stdio.h>
31
#include <stdlib.h>
32
#include <string.h>
33
#include <ctype.h>
34
#include <time.h>
35
#include <errno.h>
36
#include <sys/stat.h>
37
#include <sys/file.h>
38
39
#include <wget.h>
40
#include "private.h"
41
42
/**
43
 * \file
44
 * \brief HTTP Strict Transport Security (RFC 6797) routines
45
 * \defgroup libwget-hsts HTTP Strict Transport Security (RFC 6797) routines
46
 * @{
47
 *
48
 * This is an implementation of RFC 6797.
49
 */
50
51
struct wget_hsts_db_st {
52
  const char *
53
    fname;
54
  wget_hashmap *
55
    entries;
56
  wget_thread_mutex
57
    mutex;
58
  int64_t
59
    load_time;
60
};
61
62
typedef struct {
63
  const char *
64
    host;
65
  int64_t
66
    expires; // expiry time
67
  int64_t
68
    created; // creation time
69
  int64_t
70
    maxage; // max-age in seconds
71
  uint16_t
72
    port;
73
  bool
74
    include_subdomains : 1; // whether or not subdomains are included
75
} hsts_entry;
76
77
/// Pointer to the function table
78
static const wget_hsts_db_vtable
79
  *plugin_vtable;
80
81
void wget_hsts_set_plugin(const wget_hsts_db_vtable *vtable)
82
0
{
83
0
  plugin_vtable = vtable;
84
0
}
85
86
#ifdef __clang__
87
__attribute__((no_sanitize("integer")))
88
#endif
89
WGET_GCC_PURE
90
static unsigned int hash_hsts(const hsts_entry *hsts)
91
5.67k
{
92
5.67k
  unsigned int hash = hsts->port;
93
5.67k
  const unsigned char *p;
94
95
20.0k
  for (p = (unsigned char *)hsts->host; *p; p++)
96
14.3k
    hash = hash * 101 + *p;
97
98
5.67k
  return hash;
99
5.67k
}
100
101
WGET_GCC_NONNULL_ALL WGET_GCC_PURE
102
static int compare_hsts(const hsts_entry *h1, const hsts_entry *h2)
103
1.37k
{
104
1.37k
  int n;
105
106
1.37k
  if ((n = strcmp(h1->host, h2->host)))
107
735
    return n;
108
109
641
  return h1->port < h2->port ? -1 : (h1->port > h2->port ? 1 : 0);
110
1.37k
}
111
112
static hsts_entry *init_hsts(hsts_entry *hsts)
113
3.05k
{
114
3.05k
  if (!hsts) {
115
0
    if (!(hsts = wget_calloc(1, sizeof(hsts_entry))))
116
0
      return NULL;
117
0
  } else
118
3.05k
    memset(hsts, 0, sizeof(*hsts));
119
120
3.05k
  hsts->created = time(NULL);
121
122
3.05k
  return hsts;
123
3.05k
}
124
125
static void deinit_hsts(hsts_entry *hsts)
126
3.05k
{
127
3.05k
  if (hsts) {
128
3.05k
    xfree(hsts->host);
129
3.05k
  }
130
3.05k
}
131
132
static void free_hsts(hsts_entry *hsts)
133
1.65k
{
134
1.65k
  if (hsts) {
135
1.65k
    deinit_hsts(hsts);
136
1.65k
    xfree(hsts);
137
1.65k
  }
138
1.65k
}
139
140
static hsts_entry *new_hsts(const char *host, uint16_t port, int64_t maxage, bool include_subdomains)
141
0
{
142
0
  hsts_entry *hsts = init_hsts(NULL);
143
144
0
  if (!hsts)
145
0
    return NULL;
146
147
0
  hsts->host = wget_strdup(host);
148
0
  hsts->port = port ? port : 443;
149
0
  hsts->include_subdomains = include_subdomains;
150
151
0
  if (maxage <= 0 || maxage >= INT64_MAX / 2 || hsts->created < 0 || hsts->created >= INT64_MAX / 2) {
152
0
    hsts->maxage = 0;
153
0
    hsts->expires = 0;
154
0
  } else {
155
0
    hsts->maxage = maxage;
156
0
    hsts->expires = hsts->created + maxage;
157
0
  }
158
159
0
  return hsts;
160
0
}
161
162
/**
163
 * \param[in] hsts_db An HSTS database
164
 * \param[in] host Hostname to search for
165
 * \param[in] port Port number in the original URI/IRI.
166
 *                 Port number 80 is treated similar to 443, as 80 is default port for HTTP.
167
 * \return 1 if the host must be accessed only through TLS, 0 if there is no such condition.
168
 *
169
 * Searches for a given host in the database for any previously added entry.
170
 *
171
 * HSTS entries older than amount of time specified by `maxage` are considered `expired` and are ignored.
172
 *
173
 * This function is thread-safe and can be called from multiple threads concurrently.
174
 * Any implementation for this function must be thread-safe as well.
175
 */
176
int wget_hsts_host_match(const wget_hsts_db *hsts_db, const char *host, uint16_t port)
177
1.38k
{
178
1.38k
  if (plugin_vtable)
179
0
    return plugin_vtable->host_match(hsts_db, host, port);
180
181
1.38k
  if (!hsts_db)
182
0
    return 0;
183
184
1.38k
  hsts_entry hsts, *hstsp;
185
1.38k
  const char *p;
186
1.38k
  int64_t now = time(NULL);
187
188
  // first look for an exact match
189
  // if it's the default port, "normalize" it
190
  // we assume the scheme is HTTP
191
1.38k
  hsts.port = (port == 80 ? 443 : port);
192
1.38k
  hsts.host = host;
193
1.38k
  if (wget_hashmap_get(hsts_db->entries, &hsts, &hstsp) && hstsp->expires >= now)
194
91
    return 1;
195
196
  // now look for a valid subdomain match
197
2.37k
  for (p = host; (p = strchr(p, '.')); ) {
198
1.29k
    hsts.host = ++p;
199
1.29k
    if (wget_hashmap_get(hsts_db->entries, &hsts, &hstsp)
200
238
        && hstsp->include_subdomains && hstsp->expires >= now)
201
214
      return 1;
202
1.29k
  }
203
204
1.07k
  return 0;
205
1.29k
}
206
207
/**
208
 * \param[in] hsts_db HSTS database created by wget_hsts_db_init()
209
 *
210
 * Frees all resources allocated for HSTS database, except for the structure itself. The `hsts_db` pointer can then
211
 * be passed to wget_hsts_db_init() for reinitialization.
212
 *
213
 * If `hsts_db` is NULL this function does nothing.
214
 *
215
 * This function only works with databases created by wget_hsts_db_init().
216
 */
217
void wget_hsts_db_deinit(wget_hsts_db *hsts_db)
218
1.38k
{
219
1.38k
  if (plugin_vtable) {
220
0
    plugin_vtable->deinit(hsts_db);
221
0
    return;
222
0
  }
223
224
1.38k
  if (hsts_db) {
225
1.38k
    xfree(hsts_db->fname);
226
1.38k
    wget_thread_mutex_lock(hsts_db->mutex);
227
1.38k
    wget_hashmap_free(&hsts_db->entries);
228
1.38k
    wget_thread_mutex_unlock(hsts_db->mutex);
229
230
1.38k
    wget_thread_mutex_destroy(&hsts_db->mutex);
231
1.38k
  }
232
1.38k
}
233
234
/**
235
 * \param[in] hsts_db Pointer to the HSTS database handle (will be set to NULL)
236
 *
237
 * Frees all resources allocated for the HSTS database.
238
 *
239
 * A double pointer is required because this function will set the handle (pointer) to the HPKP database to NULL
240
 * to prevent potential use-after-free conditions.
241
 *
242
 * If `hsts_db` or pointer it points to is NULL, then the function does nothing.
243
 *
244
 * Newly added entries will be lost unless committed to persistent storage using wget_hsts_db_save().
245
 */
246
void wget_hsts_db_free(wget_hsts_db **hsts_db)
247
11.6k
{
248
11.6k
  if (plugin_vtable) {
249
0
    plugin_vtable->free(hsts_db);
250
0
    return;
251
0
  }
252
253
11.6k
  if (hsts_db && *hsts_db) {
254
1.38k
    wget_hsts_db_deinit(*hsts_db);
255
1.38k
    xfree(*hsts_db);
256
1.38k
  }
257
11.6k
}
258
259
static void hsts_db_add_entry(wget_hsts_db *hsts_db, hsts_entry *hsts)
260
1.65k
{
261
1.65k
  if (!hsts)
262
0
    return;
263
264
1.65k
  wget_thread_mutex_lock(hsts_db->mutex);
265
266
1.65k
  if (hsts->maxage == 0) {
267
0
    if (wget_hashmap_remove(hsts_db->entries, hsts)) {
268
0
      if (wget_ip_is_family(hsts->host, WGET_NET_FAMILY_IPV6))
269
0
        debug_printf("removed HSTS [%s]:%hu\n", hsts->host, hsts->port);
270
0
      else
271
0
        debug_printf("removed HSTS %s:%hu\n", hsts->host, hsts->port);
272
0
    }
273
0
    free_hsts(hsts);
274
0
    hsts = NULL;
275
1.65k
  } else {
276
1.65k
    hsts_entry *old;
277
278
1.65k
    if (wget_hashmap_get(hsts_db->entries, hsts, &old)) {
279
312
      if (old->created < hsts->created || old->maxage != hsts->maxage || old->include_subdomains != hsts->include_subdomains) {
280
261
        old->created = hsts->created;
281
261
        old->expires = hsts->expires;
282
261
        old->maxage = hsts->maxage;
283
261
        old->include_subdomains = hsts->include_subdomains;
284
261
        if (wget_ip_is_family(old->host, WGET_NET_FAMILY_IPV6))
285
20
          debug_printf("update HSTS [%s]:%hu (maxage=%lld, includeSubDomains=%d)\n", old->host, old->port, (long long) old->maxage, old->include_subdomains);
286
241
        else
287
241
          debug_printf("update HSTS %s:%hu (maxage=%lld, includeSubDomains=%d)\n", old->host, old->port, (long long) old->maxage, old->include_subdomains);
288
261
      }
289
312
      free_hsts(hsts);
290
312
      hsts = NULL;
291
1.34k
    } else {
292
      // key and value are the same to make wget_hashmap_get() return old 'hsts'
293
      // debug_printf("add HSTS %s:%hu (maxage=%lld, includeSubDomains=%d)\n", hsts->host, hsts->port, (long long)hsts->maxage, hsts->include_subdomains);
294
1.34k
      wget_hashmap_put(hsts_db->entries, hsts, hsts);
295
      // no need to free anything here
296
1.34k
    }
297
1.65k
  }
298
299
1.65k
  wget_thread_mutex_unlock(hsts_db->mutex);
300
1.65k
}
301
302
/**
303
 * \param[in] hsts_db An HSTS database
304
 * \param[in] host Hostname from where `Strict-Transport-Security` header was received
305
 * \param[in] port Port number used for connecting to the host
306
 * \param[in] maxage The time from now till the entry is valid, in seconds, or 0 to remove existing entry.
307
 *                   Corresponds to the `max-age` directive in `Strict-Transport-Security` header.
308
 * \param[in] include_subdomains Nonzero if `includeSubDomains` directive was present in the header, zero otherwise
309
 *
310
 * Add an entry to the HSTS database. An entry corresponds to the `Strict-Transport-Security` HTTP response header.
311
 * Any existing entry with same `host` and `port` is replaced. If `maxage` is zero, any existing entry with
312
 * matching `host` and `port` is removed.
313
 *
314
 * This function is thread-safe and can be called from multiple threads concurrently.
315
 * Any implementation for this function must be thread-safe as well.
316
 */
317
void wget_hsts_db_add(wget_hsts_db *hsts_db, const char *host, uint16_t port, int64_t maxage, bool include_subdomains)
318
0
{
319
0
  if (plugin_vtable) {
320
0
    plugin_vtable->add(hsts_db, host, port, maxage, include_subdomains);
321
0
    return;
322
0
  }
323
324
0
  if (hsts_db) {
325
0
    hsts_entry *hsts = new_hsts(host, port, maxage, include_subdomains);
326
327
0
    hsts_db_add_entry(hsts_db, hsts);
328
0
  }
329
0
}
330
331
static int hsts_db_load(wget_hsts_db *hsts_db, FILE *fp)
332
1.38k
{
333
1.38k
  hsts_entry hsts;
334
1.38k
  struct stat st;
335
1.38k
  char *buf = NULL, *linep, *p;
336
1.38k
  size_t bufsize = 0;
337
1.38k
  ssize_t buflen;
338
1.38k
  int64_t now = time(NULL);
339
1.38k
  int ok;
340
341
  // if the database file hasn't changed since the last read
342
  // there's no need to reload
343
344
1.38k
  if (fstat(fileno(fp), &st) == 0) {
345
0
    if (st.st_mtime != hsts_db->load_time)
346
0
      hsts_db->load_time = st.st_mtime;
347
0
    else
348
0
      return 0;
349
0
  }
350
351
5.68k
  while ((buflen = wget_getline(&buf, &bufsize, fp)) >= 0) {
352
4.29k
    linep = buf;
353
354
4.29k
    while (isspace(*linep)) linep++; // ignore leading whitespace
355
4.29k
    if (!*linep) continue; // skip empty lines
356
357
3.24k
    if (*linep == '#')
358
195
      continue; // skip comments
359
360
    // strip off \r\n
361
3.05k
    while (buflen > 0 && (buf[buflen] == '\n' || buf[buflen] == '\r'))
362
0
      buf[--buflen] = 0;
363
364
3.05k
    init_hsts(&hsts);
365
3.05k
    ok = 0;
366
367
    // parse host
368
3.05k
    if (*linep) {
369
9.59k
      for (p = linep; *linep && !isspace(*linep); )
370
6.54k
        linep++;
371
3.05k
      hsts.host = wget_strmemdup(p, linep - p);
372
3.05k
    }
373
374
    // parse port
375
3.05k
    if (*linep) {
376
3.12k
      for (p = ++linep; *linep && !isspace(*linep); )
377
459
        linep++;
378
2.66k
      hsts.port = (uint16_t) atoi(p);
379
2.66k
      if (hsts.port == 0)
380
1.05k
        hsts.port = 443;
381
2.66k
    }
382
383
    // parse includeSubDomains
384
3.05k
    if (*linep) {
385
2.91k
      for (p = ++linep; *linep && !isspace(*linep); )
386
406
        linep++;
387
2.51k
      hsts.include_subdomains = atoi(p) ? 1 : 0;
388
2.51k
    }
389
390
    // parse creation time
391
3.05k
    if (*linep) {
392
6.03k
      for (p = ++linep; *linep && !isspace(*linep); )
393
3.63k
        linep++;
394
2.40k
      hsts.created = atoll(p);
395
2.40k
      if (hsts.created < 0 || hsts.created >= INT64_MAX / 2)
396
338
        hsts.created = 0;
397
2.40k
    }
398
399
    // parse max age
400
3.05k
    if (*linep) {
401
20.8k
      for (p = ++linep; *linep && !isspace(*linep); )
402
18.8k
        linep++;
403
2.02k
      hsts.maxage = atoll(p);
404
2.02k
      if (hsts.maxage < 0 || hsts.maxage >= INT64_MAX / 2)
405
171
        hsts.maxage = 0; // avoid integer overflow here
406
2.02k
      hsts.expires = hsts.maxage ? hsts.created + hsts.maxage : 0;
407
2.02k
      if (hsts.expires < now) {
408
        // drop expired entry
409
371
        deinit_hsts(&hsts);
410
371
        continue;
411
371
      }
412
1.65k
      ok = 1;
413
1.65k
    }
414
415
2.67k
    if (ok) {
416
1.65k
      hsts_db_add_entry(hsts_db, wget_memdup(&hsts, sizeof(hsts)));
417
1.65k
    } else {
418
1.02k
      deinit_hsts(&hsts);
419
1.02k
      error_printf(_("Failed to parse HSTS line: '%s'\n"), buf);
420
1.02k
    }
421
2.67k
  }
422
423
1.38k
  xfree(buf);
424
425
1.38k
  if (ferror(fp)) {
426
0
    hsts_db->load_time = 0; // reload on next call to this function
427
0
    return -1;
428
0
  }
429
430
1.38k
  return 0;
431
1.38k
}
432
433
/**
434
 * \param[in] hsts_db An HSTS database
435
 * \return 0 if the operation succeeded, -1 in case of error
436
 *
437
 * Performs all operations necessary to access the HSTS database entries from persistent storage
438
 * using wget_hsts_host_match() for example.
439
 *
440
 * For database created by wget_hsts_db_init() this function will load all the entries from the file specified
441
 * in `fname` parameter of wget_hsts_db_init().
442
 *
443
 * If `hsts_db` is NULL this function does nothing and returns 0.
444
 */
445
int wget_hsts_db_load(wget_hsts_db *hsts_db)
446
1.38k
{
447
1.38k
  if (plugin_vtable)
448
0
    return plugin_vtable->load(hsts_db);
449
450
1.38k
  if (!hsts_db)
451
0
    return -1;
452
453
1.38k
  if (!hsts_db->fname || !*hsts_db->fname)
454
0
    return 0;
455
456
  // Load the HSTS cache from a flat file
457
  // Protected by flock()
458
1.38k
  if (wget_update_file(hsts_db->fname, (wget_update_load_fn *) hsts_db_load, NULL, hsts_db)) {
459
0
    error_printf(_("Failed to read HSTS data\n"));
460
0
    return -1;
461
1.38k
  } else {
462
1.38k
    debug_printf("Fetched HSTS data from '%s'\n", hsts_db->fname);
463
1.38k
    return 0;
464
1.38k
  }
465
1.38k
}
466
467
WGET_GCC_NONNULL_ALL
468
static int hsts_save(void *_fp, const void *_hsts, WGET_GCC_UNUSED void *v)
469
0
{
470
0
  FILE *fp = _fp;
471
0
  const hsts_entry *hsts = _hsts;
472
473
0
  wget_fprintf(fp, "%s %hu %d %lld %lld\n", hsts->host, hsts->port, hsts->include_subdomains, (long long)hsts->created, (long long)hsts->maxage);
474
0
  return 0;
475
0
}
476
477
static int hsts_db_save(void *hsts_db, FILE *fp)
478
0
{
479
0
  wget_hashmap *entries = ((wget_hsts_db *) hsts_db)->entries;
480
481
0
  if (wget_hashmap_size(entries) > 0) {
482
0
    fputs("#HSTS 1.0 file\n", fp);
483
0
    fputs("#Generated by libwget " PACKAGE_VERSION ". Edit at your own risk.\n", fp);
484
0
    fputs("# <hostname> <port> <incl. subdomains> <created> <max-age>\n", fp);
485
486
0
    wget_hashmap_browse(entries, hsts_save, fp);
487
488
0
    if (ferror(fp))
489
0
      return -1;
490
0
  }
491
492
0
  return 0;
493
0
}
494
495
/**
496
 * \param[in] hsts_db HSTS database
497
 * \return 0 if the operation succeeded, -1 otherwise
498
 *
499
 * Saves all changes to the HSTS database (via wget_hsts_db_add() for example) to persistent storage.
500
 *
501
 * For databases created by wget_hsts_db_init(), the data is stored into file specified by `fname` parameter
502
 * of wget_hsts_db_init().
503
 *
504
 * If `hsts_db` is NULL this function does nothing.
505
 */
506
int wget_hsts_db_save(wget_hsts_db *hsts_db)
507
0
{
508
0
  int size;
509
510
0
  if (plugin_vtable)
511
0
    return plugin_vtable->save(hsts_db);
512
513
0
  if (!hsts_db)
514
0
    return -1;
515
516
0
  if (!hsts_db->fname || !*hsts_db->fname)
517
0
    return -1;
518
519
  // Save the HSTS cache to a flat file
520
  // Protected by flock()
521
0
  if (wget_update_file(hsts_db->fname, (wget_update_load_fn *) hsts_db_load, hsts_db_save, hsts_db)) {
522
0
    error_printf(_("Failed to write HSTS file '%s'\n"), hsts_db->fname);
523
0
    return -1;
524
0
  }
525
526
0
  if ((size = wget_hashmap_size(hsts_db->entries)))
527
0
    debug_printf("Saved %d HSTS entr%s into '%s'\n", size, size != 1 ? "ies" : "y", hsts_db->fname);
528
0
  else
529
0
    debug_printf("No HSTS entries to save. Table is empty.\n");
530
531
0
  return 0;
532
0
}
533
534
/**
535
 * \param[in] hsts_db Previously created HSTS database on which wget_hsts_db_deinit() has been called, or NULL
536
 * \param[in] fname The file where the data is stored, or NULL.
537
 * \return A new wget_hsts_db
538
 *
539
 * Constructor for the default implementation of HSTS database.
540
 *
541
 * This function does no file IO, data is read only when \ref wget_hsts_db_load "wget_hsts_db_load()" is called.
542
 */
543
wget_hsts_db *wget_hsts_db_init(wget_hsts_db *hsts_db, const char *fname)
544
1.38k
{
545
1.38k
  if (plugin_vtable)
546
0
    return plugin_vtable->init(hsts_db, fname);
547
548
1.38k
  if (fname) {
549
1.38k
    if (!(fname = wget_strdup(fname)))
550
0
      return NULL;
551
1.38k
  }
552
553
1.38k
  wget_hashmap *entries = wget_hashmap_create(16, (wget_hashmap_hash_fn *) hash_hsts, (wget_hashmap_compare_fn *) compare_hsts);
554
1.38k
  if (!entries) {
555
0
    xfree(fname);
556
0
    return NULL;
557
0
  }
558
559
1.38k
  if (!hsts_db) {
560
1.38k
    if (!(hsts_db = wget_calloc(1, sizeof(struct wget_hsts_db_st)))) {
561
0
      wget_hashmap_free(&entries);
562
0
      xfree(fname);
563
0
      return NULL;
564
0
    }
565
1.38k
  } else
566
0
    memset(hsts_db, 0, sizeof(*hsts_db));
567
568
1.38k
  hsts_db->fname = fname;
569
1.38k
  hsts_db->entries = entries;
570
1.38k
  wget_hashmap_set_key_destructor(hsts_db->entries, (wget_hashmap_key_destructor *) free_hsts);
571
1.38k
  wget_hashmap_set_value_destructor(hsts_db->entries, (wget_hashmap_value_destructor *) free_hsts);
572
1.38k
  wget_thread_mutex_init(&hsts_db->mutex);
573
574
1.38k
  return hsts_db;
575
1.38k
}
576
577
/**
578
 * \param[in] hsts_db HSTS database created by wget_hsts_db_init().
579
 * \param[in] fname Filename where database should be stored, or NULL
580
 *
581
 * Changes the file where HSTS database entries are stored.
582
 *
583
 * Works only for the HSTS databases created by wget_hsts_db_init().
584
 * This function does no file IO, data is read or written only when wget_hsts_db_load() or wget_hsts_db_save()
585
 * is called.
586
 */
587
void wget_hsts_db_set_fname(wget_hsts_db *hsts_db, const char *fname)
588
0
{
589
  xfree(hsts_db->fname);
590
0
  hsts_db->fname = wget_strdup(fname);
591
0
}
592
593
/**@}*/