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