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