Commit message: Rolling code for version 2.21
CVE: CVE-2018-16789
CWE: CWE-703
Vulnerability description: libhttp/url.c in shellinabox through 2.20 has an implementation flaw in the HTTP request parsing logic. By sending a crafted multipart/form-data HTTP request, an attacker could exploit this to force shellinaboxd into an infinite loop, exhausting available CPU resources and taking the service down.

Project: shellinabox
File: url.c

const struct HashMap *urlGetArgs(struct URL *url) {
  return &url->args;
}

Project: shellinabox
File: url.c

const char *urlGetHost(struct URL *url) {
  return url->host;
}

Project: shellinabox
File: url.c

const char *urlGetURL(struct URL *url) {
  if (!url->url) {
    const char *host           = urlGetHost(url);
    int s_size                 = 8 + strlen(host) + 25 + strlen(url->path);
    check(*(char **)&url->url  = malloc(s_size + 1));
    *url->url                  = '\000';
    strncat(url->url, url->protocol, s_size);
    strncat(url->url, "://", s_size);
    strncat(url->url, host, s_size);
    if (url->port != (strcmp(url->protocol, "http") ? 443 : 80)) {
      snprintf(strrchr(url->url, '\000'), 25, ":%d", url->port);
    }
    strncat(url->url, url->path, s_size);
  }
  return url->url;
}

Project: shellinabox
File: url.c

static int urlMemcmp(const char *buf, int len, const char *s) {
  int sLen = strlen(s);
  if (len < sLen) {
    return s[len];
  } else {
    return memcmp(buf, s, sLen);
  }
}

Project: shellinabox
File: url.c

static void urlParseQueryString(struct HashMap *hashmap, const char *query, int len) {
  const char *key   = query;
  const char *value = NULL;
  for (const char *ampersand = query; len-- >= 0; ampersand++) {
    char ch         = len >= 0 ? *ampersand : '\000';
    if (ch == '=' && !value) {
      value         = ampersand + 1;
    } else if (ch == '&' || len < 0) {
      int kl        = (value ? value-1 : ampersand) - key;
      int vl        = value ? ampersand - value : 0;
      if (kl) {
        char *k     = urlMakeString(key, kl);
        urlUnescape(k);
        char *v     = NULL;
        if (value) {
          v         = urlMakeString(value, vl);
          urlUnescape(v);
        }
        addToHashMap(hashmap, k, v);
      }
      key           = ampersand + 1;
      value         = NULL;
    }
    if (!ch) {
      break;
    }
  }
}

Project: shellinabox
File: url.c

static void urlParsePart(struct URL *url, const char *buf, int len) {
  // Most browsers seem to forget quoting data in the header fields. This
  // means, it is quite possible for an HTML form to cause the submission of
  // unparseable "multipart/form-data". If this happens, we just give up
  // and ignore the malformed data.
  // Example:
  // <form method="POST" enctype="multipart/form-data">
  //   <input type="file" name="&quot;&#13;&#10;X: x=&quot;">
  //   <input type="submit">
  // </form>
  char *name           = NULL;
  for (const char *eol; !!(eol = urlMemstr(buf, len, "\r\n")); ) {
    if (buf == eol) {
      buf             += 2;
      len             -= 2;
      if (name) {
        char *value    = len ? urlMakeString(buf, len) : NULL;
        addToHashMap(&url->args, name, value);
        name           = NULL;
      }
      break;
    } else {
      if (!name && !urlMemcasecmp(buf, len, "content-disposition:")) {
        struct HashMap fields;
        initHashMap(&fields, urlDestroyHashMapEntry, NULL);
        urlParseHeaderLine(&fields, buf + 20, eol - buf - 20);
        if (getRefFromHashMap(&fields, "form-data")) {
          // We currently don't bother to deal with binary files (e.g. files
          // that include NUL characters). If this ever becomes necessary,
          // we could check for the existence of a "filename" field and use
          // that as an indicator to store the payload in something other
          // than "url->args".
          name         = (char *)getFromHashMap(&fields, "name");
          if (name && *name) {
            check(name = strdup(name));
          }
        }
        destroyHashMap(&fields);
      }
      len             -= eol - buf + 2;
      buf              = eol + 2;
    }
  }
  free(name);
}

Project: shellinabox
File: url.c

struct HashMap *urlParseQuery(const char *buf, int len) {
  struct HashMap *hashmap = newHashMap(urlDestroyHashMapEntry, NULL);
  urlParseQueryString(hashmap, buf, len);
  return hashmap;
}

Project: shellinabox
File: url.c

const char *urlGetPassword(struct URL *url) {
  return url->password;
}

Project: shellinabox
File: url.c

static const char *urlMemstr(const char *buf, int len, const char *s) {
  int sLen        = strlen(s);
  if (!sLen) {
    return buf;
  }
  while (len >= sLen) {
    if (len > sLen) {
      char *first = memchr(buf, *s, len - sLen);
      if (!first) {
        return NULL;
      }
      len        -= first - buf;
      buf         = first;
    }
    if (!memcmp(buf, s, sLen)) {
      return buf;
    }
    buf++;
    len--;
  }
  return NULL;
}

Project: shellinabox
File: url.c

int urlGetPort(struct URL *url) {
  return url->port;
}

Project: shellinabox
File: url.c

const char *urlGetProtocol(struct URL *url) {
  return url->protocol;
}

Project: shellinabox
File: url.c

static void urlParseHeaderLine(struct HashMap *hashmap, const char *s,
                               int len) {
  while (s && len > 0) {
    while (len > 0 && (*s == ' ' || *s == ';')) {
      s++;
      len--;
    }
    const char *key   = s;
    const char *value = NULL;
    while (len > 0 && *s != ';') {
      if (*s == '=' && value == NULL) {
        value         = s + 1;
      }
      s++;
      len--;
    }
    int kl            = (value ? value-1 : s) - key;
    int vl            = value ? s - value : 0;
    if (kl) {
      char *k         = urlMakeString(key, kl);
      for (char *t = k; *t; t++) {
        if (*t >= 'a' && *t <= 'z') {
          *t         |= 0x20;
        }
      }
      char *v         = NULL;
      if (value) {
        if (vl >= 2 && value[0] == '"' && value[vl-1] == '"') {
          value++;
          vl--;
        }
        v             = urlMakeString(value, vl);
      }
      addToHashMap(hashmap, k, v);
    }
  }
}

Project: shellinabox
File: url.c

struct URL *newURL(const struct HttpConnection *http,
                   const char *buf, int len) {
  struct URL *url;
  check(url = malloc(sizeof(struct URL)));
  initURL(url, http, buf, len);
  return url;
}

Project: shellinabox
File: url.c

static int urlMemcasecmp(const char *buf, int len, const char *s) {
  int sLen = strlen(s);
  if (len < sLen) {
    return s[len];
  } else {
    return strncasecmp(buf, s, sLen);
  }
}

Project: shellinabox
File: url.c

const char *urlGetPathInfo(struct URL *url) {
  return url->pathinfo;
}

Project: shellinabox
File: url.c

const char *urlGetAnchor(struct URL *url) {
  return url->anchor;
}

Project: shellinabox
File: url.c

const char *urlGetQuery(struct URL *url) {
  return url->query;
}

Project: shellinabox
File: url.c

static char *urlMakeString(const char *buf, int len) {
  if (!buf) {
    return NULL;
  } else {
    char *s;
    check(s = malloc(len + 1));
    memcpy(s, buf, len);
    s[len]  = '\000';
    return s;
  }
}

Project: shellinabox
File: url.c

static void urlDestroyHashMapEntry(void *arg ATTR_UNUSED, char *key,
                                   char *value) {
  UNUSED(arg);
  free(key);
  free(value);
}

Project: shellinabox
File: url.c

const char *urlGetUser(struct URL *url) {
  return url->user;
}

Project: shellinabox
File: url.c

void destroyURL(struct URL *url) {
  if (url) {
    free(url->protocol);
    free(url->user);
    free(url->password);
    free(url->host);
    free(url->path);
    free(url->pathinfo);
    free(url->query);
    free(url->anchor);
    free(url->url);
    destroyHashMap(&url->args);
  }
}

Project: shellinabox
File: url.c

void deleteURL(struct URL *url) {
  destroyURL(url);
  free(url);
}

Project: shellinabox
File: url.c

static void urlParsePostBody(struct URL *url,
                             const struct HttpConnection *http,
                             const char *buf, int len) {
  struct HashMap contentType;
  initHashMap(&contentType, urlDestroyHashMapEntry, NULL);
  const char *ctHeader     = getFromHashMap(&http->header, "content-type");
  urlParseHeaderLine(&contentType, ctHeader, ctHeader ? strlen(ctHeader) : 0);
  if (getRefFromHashMap(&contentType, "application/x-www-form-urlencoded")) {
    urlParseQueryString(&url->args, buf, len);
  } else if (getRefFromHashMap(&contentType, "multipart/form-data")) {
    const char *boundary   = getFromHashMap(&contentType, "boundary");
    if (boundary && *boundary) {
      const char *lastPart = NULL;
      for (const char *part = buf; len > 0; ) {
        const char *ptr;
        if ((part == buf && (ptr = urlMemstr(part, len, "--")) != NULL) ||
            (ptr = urlMemstr(part, len, "\r\n--")) != NULL) {
          len             -= ptr - part + (part == buf ? 2 : 4);
          part             = ptr + (part == buf ? 2 : 4);
          if (!urlMemcmp(part, len, boundary)) {
            int i          = strlen(boundary);
            len           -= i;
            part          += i;
            if (!urlMemcmp(part, len, "\r\n")) {
              len         -= 2;
              part        += 2;
              if (lastPart) {
                urlParsePart(url, lastPart, ptr - lastPart);
              } else {
                if (ptr != buf) {
                  info("[http] Ignoring prologue before \"multipart/form-data\"!");
                }
              }
              lastPart     = part;
            } else if (!urlMemcmp(part, len, "--\r\n")) {
              len         -= 4;
              part        += 4;
              urlParsePart(url, lastPart, ptr - lastPart);
              lastPart     = NULL;
              if (len > 0) {
                info("[http] Ignoring epilogue past end of \"multipart/"
				     "form-data\"!");
              }
            }
          }
        }
        /* elf-2018.09.09: Detection of broken multipart/form-data
           fixes DoS vulnerability.

           On 9/9/18 10:43 AM, Imre Rad wrote:
           Hi Markus, Marc!

           I identified a vulnerability today in Shellinabox, it is
           remote a denial of service, shellinaboxd eating up 100% cpu
           and not processing subsequent requests after the attack was
           mounted.
        */
        else {
          warn ("[http] Ignorning broken multipart/form-data");
          break;
        }
      }
      if (lastPart) {
        warn("[http] Missing final \"boundary\" for \"multipart/form-data\"!");
      }
    } else {
      warn("[http] Missing \"boundary\" information for \"multipart/form-data\"!");
    }
  }
  destroyHashMap(&contentType);
}

Project: shellinabox
File: url.c

void initURL(struct URL *url, const struct HttpConnection *http,
             const char *buf, int len) {
  url->protocol              = strdup(httpGetProtocol(http));
  url->user                  = NULL;
  url->password              = NULL;
  url->host                  = strdup(httpGetHost(http));
  url->port                  = httpGetPort(http);
  url->path                  = strdup(httpGetPath(http));
  url->pathinfo              = strdup(httpGetPathInfo(http));
  url->query                 = strdup(httpGetQuery(http));
  url->anchor                = NULL;
  url->url                   = NULL;
  initHashMap(&url->args, urlDestroyHashMapEntry, NULL);
  if (!strcmp(http->method, "GET")) {
    check(url->query);
    urlParseQueryString(&url->args, url->query, strlen(url->query));
  } else if (!strcmp(http->method, "POST")) {
    urlParsePostBody(url, http, buf, len);
  }
}

Project: shellinabox
File: url.c

const char *urlGetPath(struct URL *url) {
  return url->path;
}

Project: shellinabox
File: url.c

static char *urlUnescape(char *s) {
  int warned    = 0;
  char *r       = s;
  for (char *u  = s; *u; ) {
    char ch     = *u++;
    if (ch == '+') {
      ch        = ' ';
    } else if (ch == '%') {
      char c1   = *u;
      if ((c1 >= '0' && c1 <= '9') || ((c1 &= ~0x20) >= 'A' && c1 <= 'F')) {
        ch      = c1 - (c1 > '9' ? 'A' - 10 : '0');
        char c2 = *++u;
        if ((c2 >= '0' && c2 <= '9') || ((c2 &= ~0x20) >= 'A' && c2 <= 'F')) {
          ch    = (ch << 4) + c2 - (c2 > '9' ? 'A' - 10 : '0');
          ++u;
        } else if (!warned++) {
          warn("[http] Malformed URL encoded data \"%s\"!", r);
        }
      } else if (!warned++) {
        warn("[http] Malformed URL encoded data \"%s\"!", r);
      }
    }
    *s++        = ch;
  }
  *s            = '\000';
  return r;
}