Commit message: sanitise cache paths

Co-authored-by: JerikoOne <jeriko.one@gmx.us>
CVE: CVE-2018-14363
CWE: CWE-22
Vulnerability description: An issue was discovered in NeoMutt before 2018-07-16. newsrc.c does not properly restrict '/' characters that may have unsafe interaction with cache pathnames.

Project: neomutt
File: newsrc.c

struct NntpData *mutt_newsgroup_subscribe(struct NntpServer *nserv, char *group)
{
  struct NntpData *nntp_data = NULL;

  if (!nserv || !nserv->groups_hash || !group || !*group)
    return NULL;

  nntp_data = nntp_data_find(nserv, group);
  nntp_data->subscribed = true;
  if (!nntp_data->newsrc_ent)
  {
    nntp_data->newsrc_ent = mutt_mem_calloc(1, sizeof(struct NewsrcEntry));
    nntp_data->newsrc_len = 1;
    nntp_data->newsrc_ent[0].first = 1;
    nntp_data->newsrc_ent[0].last = 0;
  }
  return nntp_data;
}

Project: neomutt
File: newsrc.c

void nntp_group_unread_stat(struct NntpData *nntp_data)
{
  nntp_data->unread = 0;
  if (nntp_data->last_message == 0 || nntp_data->first_message > nntp_data->last_message)
    return;

  nntp_data->unread = nntp_data->last_message - nntp_data->first_message + 1;
  for (unsigned int i = 0; i < nntp_data->newsrc_len; i++)
  {
    anum_t first = nntp_data->newsrc_ent[i].first;
    if (first < nntp_data->first_message)
      first = nntp_data->first_message;
    anum_t last = nntp_data->newsrc_ent[i].last;
    if (last > nntp_data->last_message)
      last = nntp_data->last_message;
    if (first <= last)
      nntp_data->unread -= last - first + 1;
  }
}

Project: neomutt
File: newsrc.c

static int update_file(char *filename, char *buf)
{
  FILE *fp = NULL;
  char tmpfile[PATH_MAX];
  int rc = -1;

  while (true)
  {
    snprintf(tmpfile, sizeof(tmpfile), "%s.tmp", filename);
    fp = mutt_file_fopen(tmpfile, "w");
    if (!fp)
    {
      mutt_perror(tmpfile);
      *tmpfile = '\0';
      break;
    }
    if (fputs(buf, fp) == EOF)
    {
      mutt_perror(tmpfile);
      break;
    }
    if (mutt_file_fclose(&fp) == EOF)
    {
      mutt_perror(tmpfile);
      fp = NULL;
      break;
    }
    fp = NULL;
    if (rename(tmpfile, filename) < 0)
    {
      mutt_perror(filename);
      break;
    }
    *tmpfile = '\0';
    rc = 0;
    break;
  }
  if (fp)
    mutt_file_fclose(&fp);
  if (*tmpfile)
    unlink(tmpfile);
  return rc;
}

Project: neomutt
File: pop.c

void pop_fetch_mail(void)
{
  char buffer[LONG_STRING];
  char msgbuf[SHORT_STRING];
  char *url = NULL, *p = NULL;
  int delanswer, last = 0, msgs, bytes, rset = 0, ret;
  struct Connection *conn = NULL;
  struct Context ctx;
  struct Message *msg = NULL;
  struct Account acct;
  struct PopData *pop_data = NULL;

  if (!PopHost)
  {
    mutt_error(_("POP host is not defined."));
    return;
  }

  url = p = mutt_mem_calloc(strlen(PopHost) + 7, sizeof(char));
  if (url_check_scheme(PopHost) == U_UNKNOWN)
  {
    strcpy(url, "pop://");
    p = strchr(url, '\0');
  }
  strcpy(p, PopHost);

  ret = pop_parse_path(url, &acct);
  FREE(&url);
  if (ret)
  {
    mutt_error(_("%s is an invalid POP path"), PopHost);
    return;
  }

  conn = mutt_conn_find(NULL, &acct);
  if (!conn)
    return;

  pop_data = mutt_mem_calloc(1, sizeof(struct PopData));
  pop_data->conn = conn;

  if (pop_open_connection(pop_data) < 0)
  {
    mutt_socket_free(pop_data->conn);
    FREE(&pop_data);
    return;
  }

  conn->data = pop_data;

  mutt_message(_("Checking for new messages..."));

  /* find out how many messages are in the mailbox. */
  mutt_str_strfcpy(buffer, "STAT\r\n", sizeof(buffer));
  ret = pop_query(pop_data, buffer, sizeof(buffer));
  if (ret == -1)
    goto fail;
  if (ret == -2)
  {
    mutt_error("%s", pop_data->err_msg);
    goto finish;
  }

  sscanf(buffer, "+OK %d %d", &msgs, &bytes);

  /* only get unread messages */
  if (msgs > 0 && PopLast)
  {
    mutt_str_strfcpy(buffer, "LAST\r\n", sizeof(buffer));
    ret = pop_query(pop_data, buffer, sizeof(buffer));
    if (ret == -1)
      goto fail;
    if (ret == 0)
      sscanf(buffer, "+OK %d", &last);
  }

  if (msgs <= last)
  {
    mutt_message(_("No new mail in POP mailbox."));
    goto finish;
  }

  if (mx_mbox_open(NONULL(Spoolfile), MUTT_APPEND, &ctx) == NULL)
    goto finish;

  delanswer = query_quadoption(PopDelete, _("Delete messages from server?"));

  snprintf(msgbuf, sizeof(msgbuf),
           ngettext("Reading new messages (%d byte)...",
                    "Reading new messages (%d bytes)...", bytes),
           bytes);
  mutt_message("%s", msgbuf);

  for (int i = last + 1; i <= msgs; i++)
  {
    msg = mx_msg_open_new(&ctx, NULL, MUTT_ADD_FROM);
    if (!msg)
      ret = -3;
    else
    {
      snprintf(buffer, sizeof(buffer), "RETR %d\r\n", i);
      ret = pop_fetch_data(pop_data, buffer, NULL, fetch_message, msg->fp);
      if (ret == -3)
        rset = 1;

      if (ret == 0 && mx_msg_commit(&ctx, msg) != 0)
      {
        rset = 1;
        ret = -3;
      }

      mx_msg_close(&ctx, &msg);
    }

    if (ret == 0 && delanswer == MUTT_YES)
    {
      /* delete the message on the server */
      snprintf(buffer, sizeof(buffer), "DELE %d\r\n", i);
      ret = pop_query(pop_data, buffer, sizeof(buffer));
    }

    if (ret == -1)
    {
      mx_mbox_close(&ctx, NULL);
      goto fail;
    }
    if (ret == -2)
    {
      mutt_error("%s", pop_data->err_msg);
      break;
    }
    if (ret == -3)
    {
      mutt_error(_("Error while writing mailbox!"));
      break;
    }

    /* L10N: The plural is picked by the second numerical argument, i.e.
     * the %d right before 'messages', i.e. the total number of messages. */
    mutt_message(ngettext("%s [%d of %d message read]",
                          "%s [%d of %d messages read]", msgs - last),
                 msgbuf, i - last, msgs - last);
  }

  mx_mbox_close(&ctx, NULL);

  if (rset)
  {
    /* make sure no messages get deleted */
    mutt_str_strfcpy(buffer, "RSET\r\n", sizeof(buffer));
    if (pop_query(pop_data, buffer, sizeof(buffer)) == -1)
      goto fail;
  }

finish:
  /* exit gracefully */
  mutt_str_strfcpy(buffer, "QUIT\r\n", sizeof(buffer));
  if (pop_query(pop_data, buffer, sizeof(buffer)) == -1)
    goto fail;
  mutt_socket_close(conn);
  FREE(&pop_data);
  return;

fail:
  mutt_error(_("Server closed connection!"));
  mutt_socket_close(conn);
  FREE(&pop_data);
}

Project: neomutt
File: pop.c

static int pop_hcache_namer(const char *path, char *dest, size_t destlen)
{
  return snprintf(dest, destlen, "%s." HC_FEXT, path);
}

Project: neomutt
File: pop.c

static int pop_read_header(struct PopData *pop_data, struct Header *h)
{
  int rc, index;
  size_t length;
  char buf[LONG_STRING];

  FILE *f = mutt_file_mkstemp();
  if (!f)
  {
    mutt_perror("mutt_file_mkstemp failed!");
    return -3;
  }

  snprintf(buf, sizeof(buf), "LIST %d\r\n", h->refno);
  rc = pop_query(pop_data, buf, sizeof(buf));
  if (rc == 0)
  {
    sscanf(buf, "+OK %d %zu", &index, &length);

    snprintf(buf, sizeof(buf), "TOP %d 0\r\n", h->refno);
    rc = pop_fetch_data(pop_data, buf, NULL, fetch_message, f);

    if (pop_data->cmd_top == 2)
    {
      if (rc == 0)
      {
        pop_data->cmd_top = 1;

        mutt_debug(1, "set TOP capability\n");
      }

      if (rc == -2)
      {
        pop_data->cmd_top = 0;

        mutt_debug(1, "unset TOP capability\n");
        snprintf(pop_data->err_msg, sizeof(pop_data->err_msg), "%s",
                 _("Command TOP is not supported by server."));
      }
    }
  }

  switch (rc)
  {
    case 0:
    {
      rewind(f);
      h->env = mutt_rfc822_read_header(f, h, 0, 0);
      h->content->length = length - h->content->offset + 1;
      rewind(f);
      while (!feof(f))
      {
        h->content->length--;
        fgets(buf, sizeof(buf), f);
      }
      break;
    }
    case -2:
    {
      mutt_error("%s", pop_data->err_msg);
      break;
    }
    case -3:
    {
      mutt_error(_("Can't write header to temporary file!"));
      break;
    }
  }

  mutt_file_fclose(&f);
  return rc;
}