Description: Fix smtp-smuggling (CVE-2023-51766) Pull upstream changes from 4.97.1 security release. Author: Jeremy Harris Bug-Debian: https://bugs.debian.org/1059387 Origin: upstream Last-Update: 2023-12-31 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -229,10 +229,15 @@ JH/53 Bug 2743: fix immediate-delivery v JH/57 Fix control=fakreject for a custom message containing tainted data. Previously this resulted in a log complaint, due to a re-expansion present since fakereject was originally introduced. +JH/s1 Refuse to accept a line "dot, LF" as end-of-DATA unless operating in + LF-only mode (as detected from the first header line). Previously we did + accept that in (normal) CRLF mode; this has been raised as a possible + attack scenario (under the name "smtp smuggling", CVE-2023-51766). + Exim version 4.94 ----------------- JH/01 Avoid costly startup code when not strictly needed. This reduces time --- /dev/null +++ b/doc/doc-txt/cve-2023-51766 @@ -0,0 +1,69 @@ +CVE ID: CVE-2023-51766 +Date: 2016-12-15 +Credits: https://sec-consult.com/blog/detail/smtp-smuggling-spoofing-e-mails-worldwide/ +Version(s): all up to 4.97 inclusive +Issue: Given a buggy relay, Exim can be induced to accept a second message embedded + as part of the body of a first message + +Conditions +========== + +If *all* the following conditions are met + + Runtime options + --------------- + + * Exim offers PIPELINING on incoming connections + + * Exim offers CHUNKING on incoming connections + + Operation + --------- + + * DATA (as opposed to BDAT) is used for a message reception + + * The relay host sends to the Exim MTA message data including + one of "LF . LF" or "CR LF . LF" or "LF . CR LF". + + * Exim interprets the sequence as signalling the end of data for + the SMTP DATA command, and hence a first message. + + * Exim interprets further input which the relay had as message body + data, as SMTP commands and data. This could include a MAIL, RCPT, + BDAT (etc) sequence, resulting in a further message acceptance. + +Impact +====== + +One or more messages can be accepted by Exim that have not been +properly validated by the buggy relay. + +Fix +=== + +Install a fixed Exim version: + + 4.98 (once available) + 4.97.1 + +If you can't install one of the above versions, ask your package +maintainer for a version containing the backported fix. On request and +depending on our resources we will support you in backporting the fix. +(Please note, that Exim project officially doesn't support versions +prior the current stable version.) + + +Workaround +========== + + Disable CHUNKING advertisement for incoming connections. + + An attempt to "smuggle" a DATA command will trip a syncronisation + check. + +*or* + + Disable PIPELINING advertisement for incoming connections. + + The "smuggled" MAIL FROM command will then trip a syncronisation + check. --- a/src/receive.c +++ b/src/receive.c @@ -805,104 +805,118 @@ we make the CRs optional in all cases. July 2003: Bare CRs cause trouble. We now treat them as line terminators as well, so that there are no CRs in spooled messages. However, the message terminating dot is not recognized between two bare CRs. +Dec 2023: getting a site to send a body including an "LF . LF" sequence +followed by SMTP commands is a possible "smtp smuggling" attack. If +the first (header) line for the message has a proper CRLF then enforce +that for the body: convert bare LF to a space. + 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 + strict_crlf require full CRLF sequence as a line ending Returns: One of the END_xxx values indicating why it stopped reading */ static int -read_message_data_smtp(FILE *fout) +read_message_data_smtp(FILE * fout, BOOL strict_crlf) { -int ch_state = 0; -int ch; -int linelength = 0; +enum { s_linestart, s_normal, s_had_cr, s_had_nl_dot, s_had_dot_cr } ch_state = + s_linestart; +int linelength = 0, ch; while ((ch = (receive_getc)(GETC_BUFFER_UNLIMITED)) != EOF) { if (ch == 0) body_zerocount++; switch (ch_state) { - case 0: /* After LF or CRLF */ - if (ch == '.') - { - ch_state = 3; - continue; /* Don't ever write . after LF */ - } - ch_state = 1; + case s_linestart: /* After LF or CRLF */ + if (ch == '.') + { + ch_state = s_had_nl_dot; + continue; /* Don't ever write . after LF */ + } + ch_state = s_normal; - /* Else fall through to handle as normal uschar. */ + /* Else fall through to handle as normal uschar. */ - case 1: /* Normal state */ - if (ch == '\n') - { - ch_state = 0; - body_linecount++; + case s_normal: /* Normal state */ + if (ch == '\r') + { + ch_state = s_had_cr; + continue; /* Don't write the CR */ + } + if (ch == '\n') /* Bare LF at end of line */ + if (strict_crlf) + ch = ' '; /* replace LF with space */ + else + { /* treat as line ending */ + ch_state = s_linestart; + body_linecount++; + if (linelength > max_received_linelength) + max_received_linelength = linelength; + linelength = -1; + } + break; + + case s_had_cr: /* After (unwritten) CR */ + body_linecount++; /* Any char ends line */ if (linelength > max_received_linelength) - max_received_linelength = linelength; + max_received_linelength = linelength; linelength = -1; - } - else if (ch == '\r') - { - ch_state = 2; - continue; - } - break; + if (ch == '\n') /* proper CRLF */ + ch_state = s_linestart; + else + { + message_size++; /* convert the dropped CR to a stored NL */ + if (fout && fputc('\n', fout) == EOF) return END_WERROR; + cutthrough_data_put_nl(); + if (ch == '\r') /* CR; do not write */ + continue; + ch_state = s_normal; /* not LF or CR; process as standard */ + } + break; - case 2: /* After (unwritten) CR */ - body_linecount++; - if (linelength > max_received_linelength) - max_received_linelength = linelength; - linelength = -1; - if (ch == '\n') - { - ch_state = 0; - } - else - { - message_size++; - if (fout != NULL && fputc('\n', fout) == EOF) return END_WERROR; - cutthrough_data_put_nl(); - if (ch != '\r') ch_state = 1; else continue; - } - break; + case s_had_nl_dot: /* After [CR] LF . */ + if (ch == '\n') /* [CR] LF . LF */ + if (strict_crlf) + ch = ' '; /* replace LF with space */ + else + return END_DOT; + else if (ch == '\r') /* [CR] LF . CR */ + { + ch_state = s_had_dot_cr; + continue; /* Don't write the CR */ + } + /* The dot was removed on reaching s_had_nl_dot. For a doubled dot, here, + reinstate it to cutthrough. The current ch, dot or not, is passed both to + cutthrough and to file below. */ + else if (ch == '.') + { + uschar c = ch; + cutthrough_data_puts(&c, 1); + } + ch_state = s_normal; + break; - case 3: /* After [CR] LF . */ - if (ch == '\n') - return END_DOT; - if (ch == '\r') - { - ch_state = 4; - continue; - } - /* The dot was removed at state 3. For a doubled dot, here, reinstate - it to cutthrough. The current ch, dot or not, is passed both to cutthrough - and to file below. */ - if (ch == '.') - { - uschar c= ch; - cutthrough_data_puts(&c, 1); - } - ch_state = 1; - break; + case s_had_dot_cr: /* After [CR] LF . CR */ + if (ch == '\n') + return END_DOT; /* Preferred termination */ - case 4: /* After [CR] LF . CR */ - if (ch == '\n') return END_DOT; - message_size++; - body_linecount++; - if (fout != NULL && fputc('\n', fout) == EOF) return END_WERROR; - cutthrough_data_put_nl(); - if (ch == '\r') - { - ch_state = 2; - continue; - } - ch_state = 1; - break; + message_size++; /* convert the dropped CR to a stored NL */ + body_linecount++; + if (fout && fputc('\n', fout) == EOF) return END_WERROR; + cutthrough_data_put_nl(); + if (ch == '\r') + { + ch_state = s_had_cr; + continue; /* CR; do not write */ + } + ch_state = s_normal; + break; } /* Add the character to the spool file, unless skipping; then loop for the next. */ @@ -1114,11 +1128,11 @@ Returns: nothing void receive_swallow_smtp(void) { if (message_ended >= END_NOTENDED) message_ended = chunking_state <= CHUNKING_OFFERED - ? read_message_data_smtp(NULL) + ? read_message_data_smtp(NULL, FALSE) : read_message_bdat_smtp_wire(NULL); } @@ -1899,12 +1913,14 @@ for (;;) LF specially by inserting a white space after it to ensure that the header line is not terminated. */ if (ch == '\n') { - if (first_line_ended_crlf == TRUE_UNSET) first_line_ended_crlf = FALSE; - else if (first_line_ended_crlf) receive_ungetc(' '); + if (first_line_ended_crlf == TRUE_UNSET) + first_line_ended_crlf = FALSE; + else if (first_line_ended_crlf) + receive_ungetc(' '); goto EOL; } /* This is not the end of the line. If this is SMTP input and this is the first character in the line and it is a "." character, ignore it. @@ -1915,12 +1931,17 @@ 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 == '.' && f.dot_ends) { + /* leading dot while in headers-read mode */ ch = (receive_getc)(GETC_BUFFER_UNLIMITED); - if (ch == '\r') + if (ch == '\n' && first_line_ended_crlf == TRUE /* and not TRUE_UNSET */ ) + /* dot, LF but we are in CRLF mode. Attack? */ + ch = ' '; /* replace the LF with a space */ + + else if (ch == '\r') { ch = (receive_getc)(GETC_BUFFER_UNLIMITED); if (ch != '\n') { receive_ungetc(ch); @@ -1952,11 +1973,12 @@ for (;;) if (ch == '\r') { ch = (receive_getc)(GETC_BUFFER_UNLIMITED); if (ch == '\n') { - if (first_line_ended_crlf == TRUE_UNSET) first_line_ended_crlf = TRUE; + if (first_line_ended_crlf == TRUE_UNSET) + first_line_ended_crlf = TRUE; goto EOL; } /* Otherwise, put back the character after CR, and turn the bare CR into LF SP. */ @@ -3084,11 +3106,11 @@ if (cutthrough.cctx.sock >= 0 && cutthro (void) cutthrough_headers_send(); } /* Open a new spool file for the data portion of the message. We need -to access it both via a file descriptor and a stream. Try to make the +to access it both via a file descriptor and a stdio stream. Try to make the directory if it isn't there. */ spool_name = spool_fname(US"input", message_subdir, message_id, US"-D"); DEBUG(D_receive) debug_printf("Data file name: %s\n", spool_name); @@ -3153,11 +3175,11 @@ message id or "next" line. */ if (!ferror(spool_data_file) && !(receive_feof)() && message_ended != END_DOT) { if (smtp_input) { message_ended = chunking_state <= CHUNKING_OFFERED - ? read_message_data_smtp(spool_data_file) + ? read_message_data_smtp(spool_data_file, first_line_ended_crlf) : spool_wireformat ? read_message_bdat_smtp_wire(spool_data_file) : read_message_bdat_smtp(spool_data_file); receive_linecount++; /* The terminating "." line */ } --- a/src/smtp_in.c +++ b/src/smtp_in.c @@ -5393,16 +5393,16 @@ while (done <= 0) } break; } if (chunking_state > CHUNKING_OFFERED) - rc = OK; /* No predata ACL or go-ahead output for BDAT */ + rc = OK; /* There is no predata ACL or go-ahead output for BDAT */ else { - /* If there is an ACL, re-check the synchronization afterwards, since the - ACL may have delayed. To handle cutthrough delivery enforce a dummy call - to get the DATA command sent. */ + /* If there is a predata-ACL, re-check the synchronization afterwards, + since the ACL may have delayed. To handle cutthrough delivery enforce a + dummy call to get the DATA command sent. */ if (acl_smtp_predata == NULL && cutthrough.cctx.sock < 0) rc = OK; else { --- a/doc/spec.txt +++ b/doc/spec.txt @@ -32960,12 +32960,10 @@ MTA within an operating system would use has shown that this is not the case; for example, there are Unix applications that use CRLF in this circumstance. For this reason, and for compatibility with other MTAs, the way Exim handles line endings for all messages is now as follows: - * LF not preceded by CR is treated as a line ending. - * CR is treated as a line ending; if it is immediately followed by LF, the LF is ignored. * The sequence "CR, dot, CR" does not terminate an incoming SMTP message, nor a local message in the state where a line containing only a dot is a @@ -32976,11 +32974,14 @@ follows: behind this is that bare CRs in header lines are most likely either to be mistakes, or people trying to play silly games. * If the first header line received in a message ends with CRLF, a subsequent bare LF in a header line is treated in the same way as a bare CR in a - header line. + header line and a bare LF in a body line is replaced with a space. + + * If the first header line received in a message does not end with CRLF, a + subsequent LF not preceded by CR is treated as a line ending. 48.3 Unqualified addresses --------------------------