Chunking: do not treat the first lonely dot special. CVE-2017-16944, Bug 2201
[exim.git] / src / src / receive.c
index 48cdff88c711b0f6a133a98af7761d9f0429c374..2812ea2c87743f5db2171cbf86e00c6c24d08a9e 100644 (file)
@@ -2,7 +2,7 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2016 */
+/* Copyright (c) University of Cambridge 1995 - 2017 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Code for receiving a message and setting up spool files. */
@@ -25,6 +25,7 @@ static FILE   *data_file = NULL;
 static int     data_fd = -1;
 static uschar *spool_name = US"";
 
+enum CH_STATE {LF_SEEN, MID_LINE, CR_SEEN};
 
 
 /*************************************************
@@ -898,11 +899,15 @@ return END_EOF;
 
 
 /* Variant of the above read_message_data_smtp() specialised for RFC 3030
-CHUNKING.  We assume that the incoming has proper CRLF, so only have to scan
-for and strip CR.  On the downside there are more protocol reasons to stop.
+CHUNKING. Accept input lines separated by either CRLF or CR or LF and write
+LF-delimited spoolfile.  Until we have wireformat spoolfiles, we need the
+body_linecount accounting for proper re-expansion for the wire, so use
+a cut-down version of the state-machine above; we don't need to do leading-dot
+detection and unstuffing.
 
 Arguments:
-  fout      a FILE to which to write the message; NULL if skipping
+  fout      a FILE to which to write the message; NULL if skipping;
+            must be open for both writing and reading.
 
 Returns:    One of the END_xxx values indicating why it stopped reading
 */
@@ -910,43 +915,105 @@ Returns:    One of the END_xxx values indicating why it stopped reading
 static int
 read_message_bdat_smtp(FILE *fout)
 {
-int ch;
-int linelength = 0;
+int linelength = 0, ch;
+enum CH_STATE ch_state = LF_SEEN;
+BOOL fix_nl = FALSE;
 
-for (;;) switch (ch = bdat_getc(GETC_BUFFER_UNLIMITED))
+for(;;)
   {
-  case EOF: return END_EOF;
-  case EOD: return END_DOT;
-  case ERR: return END_PROTOCOL;
+  switch ((ch = (bdat_getc)(GETC_BUFFER_UNLIMITED)))
+    {
+    case EOF:  return END_EOF;
+    case ERR:  return END_PROTOCOL;
+    case EOD:
+      /* Nothing to get from the sender anymore. We check the last
+      character written to the spool.
+
+      RFC 3030 states, that BDAT chunks are normal text, terminated by CRLF.
+      If we would be strict, we would refuse such broken messages.
+      But we are liberal, so we fix it.  It would be easy just to append
+      the "\n" to the spool.
+
+      But there are some more things (line counting, message size calculation and such),
+      that would need to be duplicated here.  So we simply do some ungetc
+      trickery.
+      */
+      if (fout)
+       {
+       if (fseek(fout, -1, SEEK_CUR) < 0)      return END_PROTOCOL;
+       if (fgetc(fout) == '\n')                return END_DOT;
+       }
 
-  case '\r':
-    body_linecount++;
-    if (linelength > max_received_linelength)
-      max_received_linelength = linelength;
-    linelength = -1;
-    break;
+      if (linelength == -1)    /* \r already seen (see below) */
+        {
+        DEBUG(D_receive) debug_printf("Add missing LF\n");
+        bdat_ungetc('\n');
+        continue;
+        }
+      DEBUG(D_receive) debug_printf("Add missing CRLF\n");
+      bdat_ungetc('\r');      /* not even \r was seen */
+      fix_nl = TRUE;
 
-  case 0:
-    body_zerocount++;
-    /*FALLTHROUGH*/
-  default:
-    message_size++;
-    linelength++;
-    if (fout)
-      {
-      if (fputc(ch, fout) == EOF) return END_WERROR;
-      if (message_size > thismessage_size_limit) return END_SIZE;
-      }
-#ifdef notyet
-    if(ch == '\n')
-      (void) cutthrough_put_nl();
-    else
-      {
-      uschar c = ch;
-      (void) cutthrough_puts(&c, 1);
-      }
-#endif
-    break;
+      continue;
+    case '\0':  body_zerocount++; break;
+    }
+  switch (ch_state)
+    {
+    case LF_SEEN:                             /* After LF or CRLF */
+      ch_state = MID_LINE;
+      /* fall through to handle as normal uschar. */
+
+    case MID_LINE:                            /* Mid-line state */
+      if (ch == '\n')
+       {
+       ch_state = LF_SEEN;
+       body_linecount++;
+       if (linelength > max_received_linelength)
+         max_received_linelength = linelength;
+       linelength = -1;
+       }
+      else if (ch == '\r')
+       {
+       ch_state = CR_SEEN;
+       if (fix_nl) bdat_ungetc('\n');
+       continue;                       /* don't write CR */
+       }
+      break;
+
+    case CR_SEEN:                       /* After (unwritten) CR */
+      body_linecount++;
+      if (linelength > max_received_linelength)
+       max_received_linelength = linelength;
+      linelength = -1;
+      if (ch == '\n')
+       ch_state = LF_SEEN;
+      else
+       {
+       message_size++;
+       if (fout && fputc('\n', fout) == EOF) return END_WERROR;
+       (void) cutthrough_put_nl();
+       if (ch == '\r') continue;       /* don't write CR */
+       ch_state = MID_LINE;
+       }
+      break;
+    }
+
+  /* Add the character to the spool file, unless skipping */
+
+  message_size++;
+  linelength++;
+  if (fout)
+    {
+    if (fputc(ch, fout) == EOF) return END_WERROR;
+    if (message_size > thismessage_size_limit) return END_SIZE;
+    }
+  if(ch == '\n')
+    (void) cutthrough_put_nl();
+  else
+    {
+    uschar c = ch;
+    (void) cutthrough_puts(&c, 1);
+    }
   }
 /*NOTREACHED*/
 }
@@ -1083,7 +1150,7 @@ switch(where)
 
 if (acl_removed_headers != NULL)
   {
-  DEBUG(D_receive|D_acl) debug_printf(">>Headers removed by %s ACL:\n", acl_name);
+  DEBUG(D_receive|D_acl) debug_printf_indent(">>Headers removed by %s ACL:\n", acl_name);
 
   for (h = header_list; h != NULL; h = h->next) if (h->type != htype_old)
     {
@@ -1096,15 +1163,15 @@ if (acl_removed_headers != NULL)
       if (header_testname(h, s, Ustrlen(s), FALSE))
        {
        h->type = htype_old;
-        DEBUG(D_receive|D_acl) debug_printf("  %s", h->text);
+        DEBUG(D_receive|D_acl) debug_printf_indent("  %s", h->text);
        }
     }
   acl_removed_headers = NULL;
-  DEBUG(D_receive|D_acl) debug_printf(">>\n");
+  DEBUG(D_receive|D_acl) debug_printf_indent(">>\n");
   }
 
 if (acl_added_headers == NULL) return;
-DEBUG(D_receive|D_acl) debug_printf(">>Headers added by %s ACL:\n", acl_name);
+DEBUG(D_receive|D_acl) debug_printf_indent(">>Headers added by %s ACL:\n", acl_name);
 
 for (h = acl_added_headers; h != NULL; h = next)
   {
@@ -1115,7 +1182,7 @@ for (h = acl_added_headers; h != NULL; h = next)
     case htype_add_top:
     h->next = header_list;
     header_list = h;
-    DEBUG(D_receive|D_acl) debug_printf("  (at top)");
+    DEBUG(D_receive|D_acl) debug_printf_indent("  (at top)");
     break;
 
     case htype_add_rec:
@@ -1130,7 +1197,7 @@ for (h = acl_added_headers; h != NULL; h = next)
       }
     h->next = last_received->next;
     last_received->next = h;
-    DEBUG(D_receive|D_acl) debug_printf("  (after Received:)");
+    DEBUG(D_receive|D_acl) debug_printf_indent("  (after Received:)");
     break;
 
     case htype_add_rfc:
@@ -1145,7 +1212,7 @@ for (h = acl_added_headers; h != NULL; h = next)
        of all headers. Our current header must follow it. */
     h->next = last_received->next;
     last_received->next = h;
-    DEBUG(D_receive|D_acl) debug_printf("  (before any non-Received: or Resent-*: header)");
+    DEBUG(D_receive|D_acl) debug_printf_indent("  (before any non-Received: or Resent-*: header)");
     break;
 
     default:
@@ -1165,11 +1232,11 @@ for (h = acl_added_headers; h != NULL; h = next)
   h->type = header_checkname(h, FALSE);
   if (h->type >= 'a') h->type = htype_other;
 
-  DEBUG(D_receive|D_acl) debug_printf("  %s", header_last->text);
+  DEBUG(D_receive|D_acl) debug_printf_indent("  %s", header_last->text);
   }
 
 acl_added_headers = NULL;
-DEBUG(D_receive|D_acl) debug_printf(">>\n");
+DEBUG(D_receive|D_acl) debug_printf_indent(">>\n");
 }
 
 
@@ -1321,7 +1388,7 @@ if (rc == OK)
       {
       (void) string_format(rfc822_file_path, sizeof(rfc822_file_path),
        "%s/scan/%s/%s", spool_directory, message_id, entry->d_name);
-      debug_printf("RFC822 attachment detected: running MIME ACL for '%s'\n",
+      DEBUG(D_receive) debug_printf("RFC822 attachment detected: running MIME ACL for '%s'\n",
        rfc822_file_path);
       break;
       }
@@ -1705,8 +1772,8 @@ for (;;)
   (and sometimes lunatic messages can have ones that are 100s of K long) we
   call store_release() for strings that have been copied - if the string is at
   the start of a block (and therefore the only thing in it, because we aren't
-  doing any other gets), the block gets freed. We can only do this because we
-  know there are no other calls to store_get() going on. */
+  doing any other gets), the block gets freed. We can only do this release if
+  there were no allocations since the once that we want to free. */
 
   if (ptr >= header_size - 4)
     {
@@ -1715,9 +1782,10 @@ for (;;)
     header_size *= 2;
     if (!store_extend(next->text, oldsize, header_size))
       {
+      BOOL release_ok = store_last_get[store_pool] == next->text;
       uschar *newtext = store_get(header_size);
       memcpy(newtext, next->text, ptr);
-      store_release(next->text);
+      if (release_ok) store_release(next->text);
       next->text = newtext;
       }
     }
@@ -1759,7 +1827,7 @@ for (;;)
   prevent further reading), and break out of the loop, having freed the
   empty header, and set next = NULL to indicate no data line. */
 
-  if (ptr == 0 && ch == '.' && (smtp_input || dot_ends))
+  if (ptr == 0 && ch == '.' && dot_ends)
     {
     ch = (receive_getc)(GETC_BUFFER_UNLIMITED);
     if (ch == '\r')
@@ -2084,6 +2152,21 @@ for (;;)
       }
     }
 
+  /* Reject CHUNKING messages that do not CRLF their first header line */
+
+  if (!first_line_ended_crlf && chunking_state > CHUNKING_OFFERED)
+    {
+    log_write(L_size_reject, LOG_MAIN|LOG_REJECT, "rejected from <%s>%s%s%s%s: "
+      "Non-CRLF-terminated header, under CHUNKING: message abandoned",
+      sender_address,
+      sender_fullhost ? " H=" : "", sender_fullhost ? sender_fullhost : US"",
+      sender_ident ? " U=" : "",    sender_ident ? sender_ident : US"");
+    smtp_printf("552 Message header not CRLF terminated\r\n");
+    bdat_flush_data();
+    smtp_reply = US"";
+    goto TIDYUP;                             /* Skip to end of function */
+    }
+
   /* The line has been handled. If we have hit EOF, break out of the loop,
   indicating no pending data line. */
 
@@ -2108,7 +2191,7 @@ normal case). */
 DEBUG(D_receive)
   {
   debug_printf(">>Headers received:\n");
-  for (h = header_list->next; h != NULL; h = h->next)
+  for (h = header_list->next; h; h = h->next)
     debug_printf("%s", h->text);
   debug_printf("\n");
   }
@@ -2135,7 +2218,7 @@ if (filter_test != FTEST_NONE && header_list->next == NULL)
 /* Scan the headers to identify them. Some are merely marked for later
 processing; some are dealt with here. */
 
-for (h = header_list->next; h != NULL; h = h->next)
+for (h = header_list->next; h; h = h->next)
   {
   BOOL is_resent = strncmpic(h->text, US"resent-", 7) == 0;
   if (is_resent) contains_resent_headers = TRUE;
@@ -2351,7 +2434,7 @@ if (extract_recip)
 
   /* Now scan the headers */
 
-  for (h = header_list->next; h != NULL; h = h->next)
+  for (h = header_list->next; h; h = h->next)
     {
     if ((h->type == htype_to || h->type == htype_cc || h->type == htype_bcc) &&
         (!contains_resent_headers || strncmpic(h->text, US"resent-", 7) == 0))
@@ -2500,8 +2583,9 @@ letter and it is not used internally.
 NOTE: If ever the format of message ids is changed, the regular expression for
 checking that a string is in this format must be updated in a corresponding
 way. It appears in the initializing code in exim.c. The macro MESSAGE_ID_LENGTH
-must also be changed to reflect the correct string length. Then, of course,
-other programs that rely on the message id format will need updating too. */
+must also be changed to reflect the correct string length. The queue-sort code
+needs to know the layout. Then, of course, other programs that rely on the
+message id format will need updating too. */
 
 Ustrncpy(message_id, string_base62((long int)(message_id_tv.tv_sec)), 6);
 message_id[6] = '-';
@@ -2845,11 +2929,11 @@ We start at the second header, skipping our own Received:. This rewriting is
 documented as happening *after* recipient addresses are taken from the headers
 by the -t command line option. An added Sender: gets rewritten here. */
 
-for (h = header_list->next; h != NULL; h = h->next)
+for (h = header_list->next; h; h = h->next)
   {
   header_line *newh = rewrite_header(h, NULL, NULL, global_rewrite_rules,
     rewrite_existflags, TRUE);
-  if (newh != NULL) h = newh;
+  if (newh) h = newh;
   }