]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
hsts: make the HSTS read callback handle name dupes
authorDaniel Stenberg <daniel@haxx.se>
Thu, 2 Apr 2026 07:16:17 +0000 (09:16 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Thu, 2 Apr 2026 16:01:20 +0000 (18:01 +0200)
Now the logic for handling name duplicates and picking the longest
expiry and strictest subdomain is the same for the callback as for when
reading from file.

Also strip trailing dots from the hostname added by the callback.

A minor side-effect is that the hostname provided by the callback can
now enable subdomains by starting the name with a dot, but we discourage
using such hostnames in documentation.

Amended test 1915 to verify.

Closes #21201

docs/libcurl/opts/CURLOPT_HSTSREADFUNCTION.md
lib/hsts.c
tests/data/test1915
tests/libtest/lib1915.c

index 67129f242d4b9e0ecbc9482cc6d00e70c27e6cf0..b5a685b73bd07ed345135c2a27e752991a20f6be 100644 (file)
@@ -42,23 +42,26 @@ Pass a pointer to your callback function, as the prototype shows above.
 This callback function gets called by libcurl repeatedly when it populates the
 in-memory HSTS cache.
 
-Set the *clientp* argument with the CURLOPT_HSTSREADDATA(3) option
-or it is NULL.
-
-When this callback is invoked, the *sts* pointer points to a populated
-struct: Copy the hostname to *name* (no longer than *namelen*
-bytes). Make it null-terminated. Set *includeSubDomains* to TRUE or
-FALSE. Set *expire* to a date stamp or a zero length string for *forever*
-(wrong date stamp format might cause the name to not get accepted)
-
-The callback should return *CURLSTS_OK* if it returns a name and is
-prepared to be called again (for another host) or *CURLSTS_DONE* if it has
-no entry to return. It can also return *CURLSTS_FAIL* to signal
-error. Returning *CURLSTS_FAIL* stops the transfer from being performed
-and make *CURLE_ABORTED_BY_CALLBACK* get returned.
-
-This option does not enable HSTS, you need to use CURLOPT_HSTS_CTRL(3) to
-do that.
+Set the *clientp* argument with the CURLOPT_HSTSREADDATA(3) option or it is
+NULL.
+
+When this callback is invoked, the *sts* pointer points to a populated struct:
+Copy the hostname to *name* (no longer than *namelen* bytes). Make it
+null-terminated. Set *includeSubDomains* to TRUE or FALSE. Set *expire* to a
+date stamp or a zero length string for *forever* (wrong date stamp format
+might cause the name to not get accepted)
+
+The callback should return *CURLSTS_OK* if it returns a name and is prepared
+to be called again (for another host) or *CURLSTS_DONE* if it has no entry to
+return. It can also return *CURLSTS_FAIL* to signal error. Returning
+*CURLSTS_FAIL* stops the transfer from being performed and make
+*CURLE_ABORTED_BY_CALLBACK* get returned.
+
+This option does not enable HSTS, you need to use CURLOPT_HSTS_CTRL(3) to do
+that.
+
+The hostname provided to libcurl *should not* have a trailing dot nor leading
+dot.
 
 # DEFAULT
 
index ff21ad98d5bd62e9c2da3df590b10b9921cef7f0..400b4423da14c3d6e9b9c95a6aeae061cc7f8ce2 100644 (file)
@@ -40,7 +40,7 @@
 
 #define MAX_HSTS_LINE    4095
 #define MAX_HSTS_HOSTLEN 2048
-#define MAX_HSTS_DATELEN 256
+#define MAX_HSTS_DATELEN 17
 #define UNLIMITED        "unlimited"
 
 #if defined(DEBUGBUILD) || defined(UNITTESTS)
@@ -399,6 +399,61 @@ skipsave:
   return result;
 }
 
+/* only returns SERIOUS errors */
+static CURLcode hsts_add_host_expire(struct hsts *h,
+                                     const char *host, size_t hostlen,
+                                     const char *expire, size_t explen,
+                                     bool subdomain) /* default */
+{
+  CURLcode result = CURLE_OK;
+  struct stsentry *e;
+  char dbuf[MAX_HSTS_DATELEN + 1];
+  time_t expires = 0;
+  time_t now = time(NULL);
+
+  /* The date parser works on a null-terminated string. */
+  if(explen > MAX_HSTS_DATELEN)
+    return CURLE_BAD_FUNCTION_ARGUMENT;
+  memcpy(dbuf, expire, explen);
+  dbuf[explen] = 0;
+
+  if(!strcmp(dbuf, UNLIMITED))
+    expires = TIME_T_MAX;
+  else
+    Curl_getdate_capped(dbuf, &expires);
+
+  if(expires <= now)
+    /* this entry already expired */
+    return CURLE_OK;
+
+  if(host[0] == '.') {
+    host++;
+    hostlen--;
+    subdomain = TRUE;
+  }
+  if(hostlen && (host[hostlen - 1] == '.'))
+    /* strip off any trailing dot */
+    hostlen--;
+
+  if(hostlen) {
+    /* only add it if not already present */
+    e = Curl_hsts(h, host, hostlen, subdomain);
+    if(!e)
+      result = hsts_create(h, host, hostlen, subdomain, expires);
+    /* 'host' is not necessarily null terminated */
+    else if((hostlen == strlen(e->host) &&
+             curl_strnequal(host, e->host, hostlen))) {
+      /* the same hostname, use the largest expire time and keep the strictest
+         subdomain policy */
+      if(expires > e->expires)
+        e->expires = expires;
+      if(subdomain)
+        e->includeSubDomains = TRUE;
+    }
+  }
+  return result;
+}
+
 /* only returns SERIOUS errors */
 static CURLcode hsts_add(struct hsts *h, const char *line)
 {
@@ -415,54 +470,9 @@ static CURLcode hsts_add(struct hsts *h, const char *line)
      curlx_str_newline(&line))
     ;
   else {
-    CURLcode result = CURLE_OK;
-    bool subdomain = FALSE;
-    struct stsentry *e;
-    char dbuf[MAX_HSTS_DATELEN + 1];
-    time_t expires = 0;
-    const char *hp = curlx_str(&host);
-    size_t hlen;
-    time_t now = time(NULL);
-
-    /* The date parser works on a null-terminated string. The maximum length
-       is upheld by curlx_str_quotedword(). */
-    memcpy(dbuf, curlx_str(&date), curlx_strlen(&date));
-    dbuf[curlx_strlen(&date)] = 0;
-
-    if(!strcmp(dbuf, UNLIMITED))
-      expires = TIME_T_MAX;
-    else
-      Curl_getdate_capped(dbuf, &expires);
-
-    if(expires <= now)
-      /* this entry already expired */
-      return CURLE_OK;
-
-    if(hp[0] == '.') {
-      curlx_str_nudge(&host, 1);
-      hp = curlx_str(&host);
-      subdomain = TRUE;
-    }
-    hlen = curlx_strlen(&host);
-    if(hlen && (hp[hlen - 1] == '.'))
-      /* strip off any trailing dot */
-      curlx_str_trim(&host, 1);
-
-    /* only add it if not already present */
-    e = Curl_hsts(h, curlx_str(&host), curlx_strlen(&host), subdomain);
-    if(!e)
-      result = hsts_create(h, curlx_str(&host), curlx_strlen(&host),
-                           subdomain, expires);
-    else if(curlx_str_casecompare(&host, e->host)) {
-      /* the same hostname, use the largest expire time and keep the
-         strictest subdomain policy */
-      if(expires > e->expires)
-        e->expires = expires;
-      if(subdomain)
-        e->includeSubDomains = TRUE;
-    }
-    if(result)
-      return result;
+    return hsts_add_host_expire(h, curlx_str(&host), curlx_strlen(&host),
+                                curlx_str(&date), curlx_strlen(&date),
+                                FALSE);
   }
 
   return CURLE_OK;
@@ -485,23 +495,23 @@ static CURLcode hsts_pull(struct Curl_easy *data, struct hsts *h)
       e.namelen = sizeof(buffer) - 1;
       e.includeSubDomains = FALSE; /* default */
       e.expire[0] = 0;
+      e.expire[MAX_HSTS_DATELEN] = 0;
       e.name[0] = 0; /* to make it clean */
+      e.name[MAX_HSTS_HOSTLEN] = 0;
       sc = data->set.hsts_read(data, &e, data->set.hsts_read_userp);
       if(sc == CURLSTS_OK) {
-        time_t expires = 0;
         CURLcode result;
-        DEBUGASSERT(e.name[0]);
-        if(!e.name[0])
-          /* bail out if no name was stored */
+        const char *date = e.expire;
+        if(!e.name[0] || e.expire[MAX_HSTS_DATELEN] ||
+           e.name[MAX_HSTS_HOSTLEN])
+          /* bail out if no name was stored or if a null terminator is gone */
           return CURLE_BAD_FUNCTION_ARGUMENT;
-        if(e.expire[0])
-          Curl_getdate_capped(e.expire, &expires);
-        else
-          expires = TIME_T_MAX; /* the end of time */
-        result = hsts_create(h, e.name, strlen(e.name),
-                             /* bitfield to bool conversion: */
-                             e.includeSubDomains ? TRUE : FALSE,
-                             expires);
+        if(!date[0])
+          date = UNLIMITED;
+        result = hsts_add_host_expire(h, e.name, strlen(e.name),
+                                      date, strlen(date),
+                                      /* bitfield to bool conversion: */
+                                      e.includeSubDomains ? TRUE : FALSE);
         if(result)
           return result;
       }
index 0b993c9ea404bfd35815ba46544c22293de56ea4..8f9089b2946dc59faf95a827511035f375625081 100644 (file)
@@ -42,13 +42,13 @@ http://%HOSTIP:%NOLISTENPORT/not-there/%TESTNUMBER
 %if large-time
 [0/4] 1.example.com 25250320 01:02:03
 [1/4] 2.example.com 25250320 03:02:01
-[2/4] 3.example.com 25250319 01:02:03
+[2/4] .3.example.com 25250319 01:02:03
 %else
 [0/4] 1.example.com 20370320 01:02:03
 [1/4] 2.example.com 20370320 03:02:01
-[2/4] 3.example.com 20370319 01:02:03
+[2/4] .3.example.com 20370319 01:02:03
 %endif
-[3/4] 4.example.com unlimited
+[3/4] .4.example.com unlimited
 First request returned 7
 Second request returned 42
 </stdout>
index 393d6edcb7366d6a9547c3eb1db3879cc4d41af2..d5dd4dc2fcdfd8f62d6abe5ccacd2e19e01b6f61 100644 (file)
@@ -40,14 +40,17 @@ static CURLSTScode hstsread(CURL *curl, struct curl_hstsentry *e, void *userp)
   static const struct entry preload_hosts[] = {
 #if (SIZEOF_TIME_T < 5)
     { "1.example.com", "20370320 01:02:03" },
-    { "2.example.com", "20370320 03:02:01" },
+    { "2.example.com.", "20370320 03:02:01" },
     { "3.example.com", "20370319 01:02:03" },
+    { ".3.example.com", "20270319 01:02:03" },
 #else
     { "1.example.com", "25250320 01:02:03" },
-    { "2.example.com", "25250320 03:02:01" },
+    { "2.example.com.", "25250320 03:02:01" },
     { "3.example.com", "25250319 01:02:03" },
+    { ".3.example.com", "22250319 01:02:03" },
 #endif
-    { "4.example.com", "" },
+    { "4.example.com", "" }, /* forever */
+    { ".4.example.com", "20370319 01:02:03" },
     { NULL, NULL } /* end of list marker */
   };
 
@@ -85,7 +88,8 @@ static CURLSTScode hstswrite(CURL *curl, struct curl_hstsentry *e,
 {
   (void)curl;
   (void)userp;
-  curl_mprintf("[%zu/%zu] %s %s\n", i->index, i->total, e->name, e->expire);
+  curl_mprintf("[%zu/%zu] %s%s %s\n", i->index, i->total,
+               e->includeSubDomains ? "." : "", e->name, e->expire);
   return CURLSTS_OK;
 }