Commit message: Ensure IMAP connection is closed after a connection error.

During connection, if the server provided an illegal initial response,
Mutt "bailed", but did not actually close the connection.  The calling
code unfortunately relied on the connection status to decide to
continue with authentication, instead of checking the "bail" return
value.

This could result in authentication credentials being sent over an
unencrypted connection, without $ssl_force_tls being consulted.

Fix this by strictly closing the connection on any invalid response
during connection.  The fix is intentionally small, to ease
backporting.  A better fix would include removing the 'err_close_conn'
label, and perhaps adding return value checking in the caller (though
this change obviates the need for that).

This addresses CVE-2020-28896.  Thanks to Gabriel Salles-Loustau for
reporting the problem, and providing test cases to reproduce.
CVE: CVE-2020-28896
CWE: CWE-287
Vulnerability description: Mutt before 2.0.2 and NeoMutt before 2020-11-20 did not ensure that $ssl_force_tls was processed if an IMAP server's initial server response was invalid. The connection was not properly closed, and the code could continue attempting to authenticate. This could result in authentication credentials being exposed on an unencrypted connection, or to a machine-in-the-middle.

Project: mutt

int imap_subscribe (char *path, int subscribe)
{
  IMAP_DATA *idata;
  char buf[LONG_STRING*2];
  char mbox[LONG_STRING];
  IMAP_MBOX mx;

  if (!mx_is_imap (path) || imap_parse_path (path, &mx) || !mx.mbox)
  {
    mutt_error (_("Bad mailbox name"));
    return -1;
  }
  if (!(idata = imap_conn_find (&(mx.account), 0)))
    goto fail;

  imap_fix_path (idata, mx.mbox, buf, sizeof (buf));
  if (!*buf)
    strfcpy (buf, "INBOX", sizeof (buf));

  if (option (OPTIMAPCHECKSUBSCRIBED))
  {
    if (subscribe)
      mutt_buffy_add (path, NULL, -1);
    else
      mutt_buffy_remove (path);
  }

  if (subscribe)
    mutt_message (_("Subscribing to %s..."), buf);
  else
    mutt_message (_("Unsubscribing from %s..."), buf);
  imap_munge_mbox_name (idata, mbox, sizeof(mbox), buf);

  snprintf (buf, sizeof (buf), "%sSUBSCRIBE %s", subscribe ? "" : "UN", mbox);

  if (imap_exec (idata, buf, 0) < 0)
    goto fail;

  imap_unmunge_mbox_name(idata, mx.mbox);
  if (subscribe)
    mutt_message (_("Subscribed to %s"), mx.mbox);
  else
    mutt_message (_("Unsubscribed from %s"), mx.mbox);
  FREE (&mx.mbox);
  return 0;

fail:
  FREE (&mx.mbox);
  return -1;
}

Project: mutt

int imap_open_connection (IMAP_DATA* idata)
{
  if (mutt_socket_open (idata->conn) < 0)
    return -1;

  idata->state = IMAP_CONNECTED;

  if (imap_cmd_step (idata) != IMAP_CMD_OK)
  {
    imap_close_connection (idata);
    return -1;
  }

  if (ascii_strncasecmp ("* OK", idata->buf, 4) == 0)
  {
    if (ascii_strncasecmp ("* OK [CAPABILITY", idata->buf, 16)
        && imap_check_capabilities (idata))
      goto bail;
#if defined(USE_SSL)
    /* Attempt STARTTLS if available and desired. */
    if (!idata->conn->ssf && (option(OPTSSLFORCETLS) ||
                              mutt_bit_isset (idata->capabilities, STARTTLS)))
    {
      int rc;

      if (option(OPTSSLFORCETLS))
        rc = MUTT_YES;
      else if ((rc = query_quadoption (OPT_SSLSTARTTLS,
                                       _("Secure connection with TLS?"))) == -1)
	goto err_close_conn;
      if (rc == MUTT_YES)
      {
	if ((rc = imap_exec (idata, "STARTTLS", IMAP_CMD_FAIL_OK)) == -1)
	  goto bail;
	if (rc != -2)
	{
	  if (mutt_ssl_starttls (idata->conn))
	  {
	    mutt_error (_("Could not negotiate TLS connection"));
	    mutt_sleep (1);
	    goto err_close_conn;
	  }
	  else
	  {
	    /* RFC 2595 demands we recheck CAPABILITY after TLS completes. */
	    if (imap_exec (idata, "CAPABILITY", 0))
	      goto bail;
	  }
	}
      }
    }

    if (option(OPTSSLFORCETLS) && ! idata->conn->ssf)
    {
      mutt_error _("Encrypted connection unavailable");
      mutt_sleep (1);
      goto err_close_conn;
    }
#endif
  }
  else if (ascii_strncasecmp ("* PREAUTH", idata->buf, 9) == 0)
  {
#if defined(USE_SSL)
    /* Unless using a secure $tunnel, an unencrypted PREAUTH response
     * may be a MITM attack.  The only way to stop "STARTTLS" MITM
     * attacks is via $ssl_force_tls: an attacker can easily spoof
     * "* OK" and strip the STARTTLS capability.  So consult
     * $ssl_force_tls, not $ssl_starttls, to decide whether to
     * abort. Note that if using $tunnel and $tunnel_is_secure,
     * conn->ssf will be set to 1. */
    if (!idata->conn->ssf && option(OPTSSLFORCETLS))
    {
      mutt_error _("Encrypted connection unavailable");
      mutt_sleep (1);
      goto err_close_conn;
    }
#endif

    idata->state = IMAP_AUTHENTICATED;
    if (imap_check_capabilities (idata) != 0)
      goto bail;
    FREE (&idata->capstr);
  }
  else
  {
    imap_error ("imap_open_connection()", idata->buf);
    goto bail;
  }

  return 0;

#if defined(USE_SSL)
err_close_conn:
#endif
bail:
  imap_close_connection (idata);
  FREE (&idata->capstr);
  return -1;
}

Project: mutt

int imap_read_literal (FILE* fp, IMAP_DATA* idata, unsigned int bytes, progress_t* pbar)
{
  unsigned int pos;
  char c;

  int r = 0;

  dprint (2, (debugfile, "imap_read_literal: reading %ld bytes\n", bytes));

  for (pos = 0; pos < bytes; pos++)
  {
    if (mutt_socket_readchar (idata->conn, &c) != 1)
    {
      dprint (1, (debugfile, "imap_read_literal: error during read, %ld bytes read\n", pos));
      idata->status = IMAP_FATAL;

      return -1;
    }

    /* Strip \r from \r\n, apparantly even literals use \r\n-terminated
      strings ?! */
    if (r == 1 && c != '\n')
      fputc ('\r', fp);

    if (c == '\r')
    {
      r = 1;
      continue;
    }
    else
      r = 0;

    fputc (c, fp);

    if (pbar && !(pos % 1024))
      mutt_progress_update (pbar, pos, -1);
#ifdef DEBUG
    if (debuglevel >= IMAP_LOG_LTRL)
      fputc (c, debugfile);
#endif
  }

  return 0;
}

Project: mutt

int imap_reconnect (IMAP_DATA **p_idata)
{
  CONTEXT *orig_ctx, new_ctx;
  int rc = -1, i;
  IMAP_DATA *idata = *p_idata;
  HEADER *old_hdr, *new_hdr;

  /* L10N:
     Message displayed when IMAP connection is lost and Mutt
     tries to reconnect.
  */
  mutt_message _("Trying to reconnect...");
  mutt_sleep (0);

  orig_ctx = idata->ctx;
  if (!orig_ctx)
    goto cleanup;

  if (mx_open_mailbox (orig_ctx->path,
                       orig_ctx->readonly ? MUTT_READONLY : 0,
                       &new_ctx) == NULL)
    goto cleanup;

  new_ctx.dontwrite = orig_ctx->dontwrite;
  new_ctx.pattern = orig_ctx->pattern;
  new_ctx.limit_pattern = orig_ctx->limit_pattern;

  orig_ctx->pattern = NULL;
  orig_ctx->limit_pattern = NULL;

  if (idata->uid_validity == ((IMAP_DATA *) new_ctx.data)->uid_validity)
  {
    for (i = 0; i < new_ctx.msgcount; i++)
    {
      new_hdr = new_ctx.hdrs[i];
      old_hdr = (HEADER *) int_hash_find (idata->uid_hash,
                                          HEADER_DATA(new_hdr)->uid);
      if (!old_hdr)
        continue;

      /* this logic is in part from mbox.c. */
      if (old_hdr->changed)
      {
        mutt_set_flag (&new_ctx, new_hdr, MUTT_FLAG, old_hdr->flagged);
        mutt_set_flag (&new_ctx, new_hdr, MUTT_REPLIED, old_hdr->replied);
        mutt_set_flag (&new_ctx, new_hdr, MUTT_OLD, old_hdr->old);
        mutt_set_flag (&new_ctx, new_hdr, MUTT_READ, old_hdr->read);

        /* TODO: the ->env check is unfortunately voodoo that I
         * haven't taken the time to track down yet.  It's in other
         * parts of the code but I don't know why yet. */
        if (old_hdr->env && old_hdr->env->changed)
        {
          new_hdr->env->changed = old_hdr->env->changed;
          new_hdr->changed = 1;
          new_ctx.changed = 1;

          if (old_hdr->env->changed & MUTT_ENV_CHANGED_IRT)
          {
            mutt_free_list (&new_hdr->env->in_reply_to);
            new_hdr->env->in_reply_to = old_hdr->env->in_reply_to;
            old_hdr->env->in_reply_to = NULL;
          }
          if (old_hdr->env->changed & MUTT_ENV_CHANGED_REFS)
          {
            mutt_free_list (&new_hdr->env->references);
            new_hdr->env->references = old_hdr->env->references;
            old_hdr->env->references = NULL;
          }
          if (old_hdr->env->changed & MUTT_ENV_CHANGED_XLABEL)
          {
            FREE (&new_hdr->env->x_label);
            new_hdr->env->x_label = old_hdr->env->x_label;
            old_hdr->env->x_label = NULL;
          }
          if (old_hdr->env->changed & MUTT_ENV_CHANGED_SUBJECT)
          {
            FREE (&new_hdr->env->subject);
            new_hdr->env->subject = old_hdr->env->subject;
            new_hdr->env->real_subj = old_hdr->env->real_subj;
            old_hdr->env->subject = old_hdr->env->real_subj = NULL;
          }
        }

        if (old_hdr->attach_del)
        {
          if (old_hdr->content->parts && !new_hdr->content->parts)
          {
            new_hdr->attach_del = 1;
            new_hdr->changed = 1;
            new_ctx.changed = 1;
            new_hdr->content->parts = old_hdr->content->parts;
            old_hdr->content->parts = NULL;
          }
        }
      }

      mutt_set_flag (&new_ctx, new_hdr, MUTT_DELETE, old_hdr->deleted);
      mutt_set_flag (&new_ctx, new_hdr, MUTT_PURGE, old_hdr->purge);
      mutt_set_flag (&new_ctx, new_hdr, MUTT_TAG, old_hdr->tagged);
    }
  }

  rc = 0;

cleanup:
  idata->status = IMAP_FATAL;
  mx_fastclose_mailbox (orig_ctx);
  imap_close_connection (idata);

  if (rc != 0)
  {
    /* L10N:
       Message when Mutt tries to reconnect to an IMAP mailbox but is
       unable to.
    */
    mutt_error _("Reconnect failed.  Mailbox closed.");
  }
  else
  {
    memcpy (orig_ctx, &new_ctx, sizeof(CONTEXT));
    idata = (IMAP_DATA *)orig_ctx->data;
    idata->ctx = orig_ctx;
    *p_idata = idata;
    /* L10N:
       Message when Mutt reconnects to an IMAP mailbox after a fatal error.
    */
    mutt_error _("Reconnect succeeded.");
  }
  mutt_sleep (0);

  return rc;
}

Project: mutt

int imap_check_mailbox (CONTEXT *ctx, int *index_hint, int force)
{
  /* overload keyboard timeout to avoid many mailbox checks in a row.
   * Most users don't like having to wait exactly when they press a key. */
  IMAP_DATA* idata;
  int result = -1, poll_rc;

  idata = (IMAP_DATA*) ctx->data;

  /* try IDLE first, unless force is set */
  if (!force && option (OPTIMAPIDLE) && mutt_bit_isset (idata->capabilities, IDLE)
      && (idata->state != IMAP_IDLE || time(NULL) >= idata->lastread + ImapKeepalive))
  {
    if (imap_cmd_idle (idata) < 0)
      goto errcleanup;
  }
  if (idata->state == IMAP_IDLE)
  {
    while ((poll_rc = mutt_socket_poll (idata->conn, 0)) > 0)
    {
      if (imap_cmd_step (idata) != IMAP_CMD_CONTINUE)
      {
        dprint (1, (debugfile, "Error reading IDLE response\n"));
        goto errcleanup;
      }
    }
    if (poll_rc < 0)
    {
      dprint (1, (debugfile, "Poll failed, disabling IDLE\n"));
      mutt_bit_unset (idata->capabilities, IDLE);
    }
  }

  if ((force ||
       (idata->state != IMAP_IDLE && time(NULL) >= idata->lastread + Timeout))
      && imap_exec (idata, "NOOP", IMAP_CMD_POLL) != 0)
    goto errcleanup;

  /* We call this even when we haven't run NOOP in case we have pending
   * changes to process, since we can reopen here. */
  imap_cmd_finish (idata);

  result = 0;

errcleanup:
  /* Try to reconnect Context if a cmd_handle_fatal() was flagged */
  if (idata->status == IMAP_FATAL)
  {
    if ((idata->reopen & IMAP_REOPEN_ALLOW) &&
        Context &&
        idata->ctx == Context)
    {
      if (imap_reconnect (&idata) == 0)
      {
        idata->check_status = 0;
        return MUTT_RECONNECTED;
      }
    }
    return -1;
  }

  if (idata->check_status & IMAP_EXPUNGE_PENDING)
    result = MUTT_REOPENED;
  else if (idata->check_status & IMAP_NEWMAIL_PENDING)
    result = MUTT_NEW_MAIL;
  else if (idata->check_status & IMAP_FLAGS_PENDING)
    result = MUTT_FLAGS;

  idata->check_status = 0;

  return result;
}