]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
cookie: refactor parse_cookie_header
authorDaniel Stenberg <daniel@haxx.se>
Wed, 3 Jun 2026 06:10:14 +0000 (08:10 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Wed, 3 Jun 2026 07:42:52 +0000 (09:42 +0200)
- introduce a few static helper functions
- simplify the bad octet checks
- simplify the too long cookie/value check

Closes #21854

lib/cookie.c

index 63615a496c91cef510e23044cc8d459a6845d737..b288a2c1d06afa25b3c848ba17fb46e20c8ef32b 100644 (file)
@@ -346,9 +346,9 @@ static bool bad_domain(const char *domain, size_t len)
 static bool invalid_octets(const char *ptr, size_t len)
 {
   const unsigned char *p = (const unsigned char *)ptr;
-  /* Reject all bytes \x01 - \x1f (*except* \x09, TAB) + \x7f */
+  /* Reject all bytes \x01 - \x1f + \x7f */
   while(len && *p) {
-    if(((*p != 9) && (*p < 0x20)) || (*p == 0x7f))
+    if((*p < 0x20) || (*p == 0x7f))
       return TRUE;
     p++;
     len--;
@@ -413,19 +413,189 @@ static CURLcode storecookie(struct Cookie *co, const struct Curl_str *cp,
   return result;
 }
 
-/* this function return errors on OOM etc, not on plain cookie format
-   problems */
-static CURLcode parse_cookie_header(
-  struct Curl_easy *data,
-  struct Cookie *co,
-  const struct CookieInfo *ci,
-  bool *okay,         /* if the cookie was fine */
-  const char *ptr,
-  const char *domain, /* default domain */
-  const char *path,   /* full path used when this cookie is
-                         set, used to get default path for
-                         the cookie unless set */
-  bool secure)        /* TRUE if connection is over secure origin */
+/*
+ * Parse the first name/value pair of the cookie header, which is the actual
+ * cookie name and value.
+ */
+static bool parse_first_pair(struct Curl_easy *data, struct Cookie *co,
+                             struct Curl_str *cookie,
+                             struct Curl_str *name,
+                             struct Curl_str *val,
+                             bool sep)
+{
+  /* The first name/value pair is the actual cookie name */
+  if(!sep || !curlx_strlen(name)) {
+    infof(data, "invalid cookie, dropped");
+    return FALSE;
+  }
+
+  /*
+   * Check for too long individual name or contents. Chrome and Firefox
+   * support 4095 or 4096 bytes combo
+   */
+  if((curlx_strlen(name) + curlx_strlen(val)) > MAX_NAME) {
+    infof(data, "oversized cookie dropped, name/val %zu + %zu bytes",
+          curlx_strlen(name), curlx_strlen(val));
+    return FALSE;
+  }
+
+  /* Check if we have a reserved prefix set. */
+  if(!strncmp("__Secure-", curlx_str(name), 9))
+    co->prefix_secure = TRUE;
+  else if(!strncmp("__Host-", curlx_str(name), 7))
+    co->prefix_host = TRUE;
+
+  cookie[COOKIE_NAME] = *name;
+  cookie[COOKIE_VALUE] = *val;
+  return TRUE;
+}
+
+static bool parse_flag(struct Curl_easy *data, struct Cookie *co,
+                       const struct CookieInfo *ci,
+                       struct Curl_str *name, bool secure)
+{
+  /*
+   * secure cookies are only allowed to be set when the connection is
+   * using a secure protocol, or when the cookie is being set by
+   * reading from file
+   */
+  if(curlx_str_casecompare(name, "secure")) {
+    if(secure || !ci->running)
+      co->secure = TRUE;
+    else {
+      infof(data, "skipped cookie because not 'secure'");
+      return FALSE;
+    }
+  }
+  else if(curlx_str_casecompare(name, "httponly"))
+    co->httponly = TRUE;
+
+  return TRUE;
+}
+
+static bool parse_domain(struct Curl_easy *data, struct Cookie *co,
+                         struct Curl_str *cookie_domain,
+                         struct Curl_str *val,
+                         const char **domainp)
+{
+  bool is_ip;
+  const char *domain = *domainp;
+  const char *v = curlx_str(val);
+  /*
+   * Now, we make sure that our host is within the given domain, or
+   * the given domain is not valid and thus cannot be set.
+   */
+
+  if('.' == *v)
+    curlx_str_nudge(val, 1);
+
+#ifndef USE_LIBPSL
+  /*
+   * Without PSL we do not know when the incoming cookie is set on a
+   * TLD or otherwise "protected" suffix. To reduce risk, we require a
+   * dot OR the exact hostname being "localhost".
+   */
+  if(bad_domain(curlx_str(val), curlx_strlen(val))) {
+    *domainp = ":";
+    domain = ":";
+  }
+#endif
+
+  is_ip = Curl_host_is_ipnum(domain ? domain : curlx_str(val));
+
+  if(!domain ||
+     (is_ip &&
+      !strncmp(curlx_str(val), domain, curlx_strlen(val)) &&
+      (curlx_strlen(val) == strlen(domain))) ||
+     (!is_ip && cookie_tailmatch(curlx_str(val),
+                                  curlx_strlen(val), domain))) {
+    *cookie_domain = *val;
+    if(!is_ip)
+      co->tailmatch = TRUE; /* we always do that if the domain name was
+                               given */
+  }
+  else {
+    /*
+     * We did not get a tailmatch and then the attempted set domain is
+     * not a domain to which the current host belongs. Mark as bad.
+     */
+    infof(data, "skipped cookie with bad tailmatch domain: %s",
+          curlx_str(val));
+    return FALSE;
+  }
+  return TRUE;
+}
+
+static void parse_maxage(struct Cookie *co, struct Curl_str *val,
+                         time_t *nowp)
+{
+  int rc;
+  const char *maxage = curlx_str(val);
+  if(*maxage == '\"')
+    maxage++;
+  rc = curlx_str_number(&maxage, &co->expires, CURL_OFF_T_MAX);
+  if(!*nowp)
+    *nowp = time(NULL);
+  switch(rc) {
+  case STRE_OVERFLOW:
+    /* overflow, used max value */
+    co->expires = CURL_OFF_T_MAX;
+    break;
+  default:
+    /* negative or otherwise bad, expire */
+    co->expires = 1;
+    break;
+  case STRE_OK:
+    if(!co->expires)
+      co->expires = 1; /* expire now */
+    else if(CURL_OFF_T_MAX - *nowp < co->expires)
+      /* would overflow */
+      co->expires = CURL_OFF_T_MAX;
+    else
+      co->expires += *nowp;
+    break;
+  }
+  cap_expires(*nowp, co);
+}
+
+static void parse_expires(struct Cookie *co, struct Curl_str *val,
+                          time_t *nowp)
+{
+  /*
+   * Let max-age have priority.
+   *
+   * If the date cannot get parsed for whatever reason, the cookie
+   * will be treated as a session cookie
+   */
+  if(!co->expires && (curlx_strlen(val) < MAX_DATE_LENGTH)) {
+    char dbuf[MAX_DATE_LENGTH + 1];
+    time_t date = 0;
+    memcpy(dbuf, curlx_str(val), curlx_strlen(val));
+    dbuf[curlx_strlen(val)] = 0;
+    if(!Curl_getdate_capped(dbuf, &date)) {
+      if(!date)
+        date++;
+      co->expires = (curl_off_t)date;
+    }
+    else
+      co->expires = 0;
+    if(!*nowp)
+      *nowp = time(NULL);
+    cap_expires(*nowp, co);
+  }
+}
+
+/* this function returns errors on OOM etc, not for cookie format problems */
+static CURLcode
+parse_cookie_header(struct Curl_easy *data,
+                    struct Cookie *co,
+                    const struct CookieInfo *ci,
+                    bool *okay, /* if the cookie was fine */
+                    const char *ptr, /* the header */
+                    const char *domain, /* default domain */
+                    /* full path used when this cookie is set */
+                    const char *path,
+                    bool secure_origin)
 {
   /* This line was read off an HTTP-header */
   time_t now = 0;
@@ -441,22 +611,25 @@ static CURLcode parse_cookie_header(
   memset(cookie, 0, sizeof(cookie));
   do {
     struct Curl_str name;
-    struct Curl_str val;
 
     /* we have a <name>=<value> pair or a stand-alone word here */
     if(!curlx_str_cspn(&ptr, &name, ";\t\r\n=")) {
+      struct Curl_str val;
       bool sep = FALSE;
       curlx_str_trimblanks(&name);
 
+      if(invalid_octets(curlx_str(&name), curlx_strlen(&name))) {
+        infof(data, "invalid octets in name, cookie dropped");
+        return CURLE_OK;
+      }
+
       if(!curlx_str_single(&ptr, '=')) {
         sep = TRUE; /* a '=' was used */
         if(!curlx_str_cspn(&ptr, &val, ";\r\n"))
           curlx_str_trimblanks(&val);
 
-        /* Reject cookies with a TAB inside the value */
-        if(curlx_strlen(&val) &&
-           memchr(curlx_str(&val), '\t', curlx_strlen(&val))) {
-          infof(data, "cookie contains TAB, dropping");
+        if(invalid_octets(curlx_str(&val), curlx_strlen(&val))) {
+          infof(data, "invalid octets in value, cookie dropped");
           return CURLE_OK;
         }
       }
@@ -464,167 +637,23 @@ static CURLcode parse_cookie_header(
         curlx_str_init(&val);
 
       if(!curlx_strlen(&cookie[COOKIE_NAME])) {
-        /* The first name/value pair is the actual cookie name */
-        if(!sep ||
-           /* Bad name/value pair. */
-           invalid_octets(curlx_str(&name), curlx_strlen(&name)) ||
-           invalid_octets(curlx_str(&val), curlx_strlen(&val)) ||
-           !curlx_strlen(&name)) {
-          infof(data, "invalid octets in name/value, cookie dropped");
-          return CURLE_OK;
-        }
-
-        /*
-         * Check for too long individual name or contents, or too long
-         * combination of name + contents. Chrome and Firefox support 4095 or
-         * 4096 bytes combo
-         */
-        if(curlx_strlen(&name) >= (MAX_NAME - 1) ||
-           curlx_strlen(&val) >= (MAX_NAME - 1) ||
-           ((curlx_strlen(&name) + curlx_strlen(&val)) > MAX_NAME)) {
-          infof(data, "oversized cookie dropped, name/val %zu + %zu bytes",
-                curlx_strlen(&name), curlx_strlen(&val));
+        if(!parse_first_pair(data, co, cookie, &name, &val, sep))
           return CURLE_OK;
-        }
-
-        /* Check if we have a reserved prefix set. */
-        if(!strncmp("__Secure-", curlx_str(&name), 9))
-          co->prefix_secure = TRUE;
-        else if(!strncmp("__Host-", curlx_str(&name), 7))
-          co->prefix_host = TRUE;
-
-        cookie[COOKIE_NAME] = name;
-        cookie[COOKIE_VALUE] = val;
       }
       else if(!sep) {
-        /*
-         * this is a "<name>" with no content
-         */
-
-        /*
-         * secure cookies are only allowed to be set when the connection is
-         * using a secure protocol, or when the cookie is being set by
-         * reading from file
-         */
-        if(curlx_str_casecompare(&name, "secure")) {
-          if(secure || !ci->running)
-            co->secure = TRUE;
-          else {
-            infof(data, "skipped cookie because not 'secure'");
-            return CURLE_OK;
-          }
-        }
-        else if(curlx_str_casecompare(&name, "httponly"))
-          co->httponly = TRUE;
+        if(!parse_flag(data, co, ci, &name, secure_origin))
+          return CURLE_OK;
       }
-      else if(curlx_str_casecompare(&name, "path")) {
+      else if(curlx_str_casecompare(&name, "path"))
         cookie[COOKIE_PATH] = val;
-      }
       else if(curlx_str_casecompare(&name, "domain") && curlx_strlen(&val)) {
-        bool is_ip;
-        const char *v = curlx_str(&val);
-        /*
-         * Now, we make sure that our host is within the given domain, or
-         * the given domain is not valid and thus cannot be set.
-         */
-
-        if('.' == *v)
-          curlx_str_nudge(&val, 1);
-
-#ifndef USE_LIBPSL
-        /*
-         * Without PSL we do not know when the incoming cookie is set on a
-         * TLD or otherwise "protected" suffix. To reduce risk, we require a
-         * dot OR the exact hostname being "localhost".
-         */
-        if(bad_domain(curlx_str(&val), curlx_strlen(&val)))
-          domain = ":";
-#endif
-
-        is_ip = Curl_host_is_ipnum(domain ? domain : curlx_str(&val));
-
-        if(!domain ||
-           (is_ip &&
-            !strncmp(curlx_str(&val), domain, curlx_strlen(&val)) &&
-            (curlx_strlen(&val) == strlen(domain))) ||
-           (!is_ip && cookie_tailmatch(curlx_str(&val),
-                                          curlx_strlen(&val), domain))) {
-          cookie[COOKIE_DOMAIN] = val;
-          if(!is_ip)
-            co->tailmatch = TRUE; /* we always do that if the domain name was
-                                     given */
-        }
-        else {
-          /*
-           * We did not get a tailmatch and then the attempted set domain is
-           * not a domain to which the current host belongs. Mark as bad.
-           */
-          infof(data, "skipped cookie with bad tailmatch domain: %s",
-                curlx_str(&val));
+        if(!parse_domain(data, co, &cookie[COOKIE_DOMAIN], &val, &domain))
           return CURLE_OK;
-        }
-      }
-      else if(curlx_str_casecompare(&name, "max-age") && curlx_strlen(&val)) {
-        /*
-         * Defined in RFC2109:
-         *
-         * Optional. The Max-Age attribute defines the lifetime of the
-         * cookie, in seconds. The delta-seconds value is a decimal non-
-         * negative integer. After delta-seconds seconds elapse, the
-         * client should discard the cookie. A value of zero means the
-         * cookie should be discarded immediately.
-         */
-        int rc;
-        const char *maxage = curlx_str(&val);
-        if(*maxage == '\"')
-          maxage++;
-        rc = curlx_str_number(&maxage, &co->expires, CURL_OFF_T_MAX);
-        if(!now)
-          now = time(NULL);
-        switch(rc) {
-        case STRE_OVERFLOW:
-          /* overflow, used max value */
-          co->expires = CURL_OFF_T_MAX;
-          break;
-        default:
-          /* negative or otherwise bad, expire */
-          co->expires = 1;
-          break;
-        case STRE_OK:
-          if(!co->expires)
-            co->expires = 1; /* expire now */
-          else if(CURL_OFF_T_MAX - now < co->expires)
-            /* would overflow */
-            co->expires = CURL_OFF_T_MAX;
-          else
-            co->expires += now;
-          break;
-        }
-        cap_expires(now, co);
-      }
-      else if(curlx_str_casecompare(&name, "expires") && curlx_strlen(&val) &&
-              !co->expires && (curlx_strlen(&val) < MAX_DATE_LENGTH)) {
-        /*
-         * Let max-age have priority.
-         *
-         * If the date cannot get parsed for whatever reason, the cookie
-         * will be treated as a session cookie
-         */
-        char dbuf[MAX_DATE_LENGTH + 1];
-        time_t date = 0;
-        memcpy(dbuf, curlx_str(&val), curlx_strlen(&val));
-        dbuf[curlx_strlen(&val)] = 0;
-        if(!Curl_getdate_capped(dbuf, &date)) {
-          if(!date)
-            date++;
-          co->expires = (curl_off_t)date;
-        }
-        else
-          co->expires = 0;
-        if(!now)
-          now = time(NULL);
-        cap_expires(now, co);
       }
+      else if(curlx_str_casecompare(&name, "max-age") && curlx_strlen(&val))
+        parse_maxage(co, &val, &now);
+      else if(curlx_str_casecompare(&name, "expires") && curlx_strlen(&val))
+        parse_expires(co, &val, &now);
     }
   } while(!curlx_str_single(&ptr, ';'));
 
@@ -641,8 +670,7 @@ static CURLcode parse_netscape(struct Cookie *co,
                                const struct CookieInfo *ci,
                                bool *okay,
                                const char *lineptr,
-                               bool secure) /* TRUE if connection is over
-                                               secure origin */
+                               bool secure_origin)
 {
   /*
    * This line is NOT an HTTP header style line, we do offer support for
@@ -715,7 +743,7 @@ static CURLcode parse_netscape(struct Cookie *co,
     case 3:
       co->secure = FALSE;
       if(curl_strnequal(ptr, "TRUE", len)) {
-        if(secure || ci->running)
+        if(secure_origin || ci->running)
           co->secure = TRUE;
         else
           return CURLE_OK;