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