Baseline for back porting the SMTP smuggling fixes to Postfix
3.8.5, 3.7.10, 3.6.14, and 3.5.24.
+
+20240124
+
+ Feature: with "smtpd_forbid_bare_newline = note", the Postfix
+ SMTP server notes in the log if it received any lines with
+ bare LF. Otherwise, "note" is like "normalize". The
+ information is formatted as "disconnect from name[address]
+ ... notes=bare_lf". The new value is expected to become
+ a list of comma-separated names. Files: smtpd/smtpd.[hc].
+
+ Cleanup: require that a stable release disables SNAPSHOT
+ and NONPROD features. File: mantools/check-snapshot-nonprod.
+
+ Bugfix (defect introduced: Postfix 3.4): the SMTP server's
+ BDAT command handler could be tricked to read $message_size_limit
+ bytes into memory. Found during code maintenance. File:
+ smtpd/smtpd.c.
+
+ Feature: never too late, an SMTP server HELP command that
+ lists the implemented commands. Some commands may be
+ implemented but not available due to smtpd_discard_ehlo_keywords
+ or access control limitations. Files: smtpd/smtpd.[hc],
+ util/argv.[hc].
+
+ Workaround: some OS lies under load: it says that a socket
+ is readable, then it says that the socket has unread data,
+ and then it says that read returns EOF, causing Postfix to
+ spam the log with a warning message. File: tlsmgr/tlsmgr.c.
# Some checks require a bin/postconf executable.
pre-release-checks: typo-check missing-proxy-read-maps-check \
postlink-check postfix-files-check check-spell-history \
- check-double-history check-table-proto check-see-postconf-d-output
+ check-double-history check-table-proto check-see-postconf-d-output \
+ check-snapshot-nonprod
postfix-files-check:
mantools/check-postfix-files | diff /dev/null -
check-see-postconf-d-output:
mantools/check-see-postconf-d-output | diff /dev/null -
+check-snapshot-nonprod:
+ mantools/check-snapshot-nonprod
+
# The build-time shlib_directory setting must take precedence over
# the installed main.cf settings, otherwise we can't update an
# installed system from dynamicmaps=yes<->dynamicmaps=no or from
<CR><LF>.<CR><LF>. <br> <br> Such clients
can be excluded with <a href="postconf.5.html#smtpd_forbid_bare_newline_exclusions">smtpd_forbid_bare_newline_exclusions</a>. </dd>
+<dt> <b>note</b> </dt> <dd> Same as "normalize", but also notes in
+the log whether the Postfix SMTP server received any lines with
+"bare <LF>". The information is formatted as "<tt>disconnect
+from name[address] ... notes=bare_lf</tt>". The new value is
+expected to become a list of comma-separated names. <br> <br> This
+feature is available in Postfix 3.9 and later. </dd>
+
<dt> <b>yes</b> </dt> <dd> Compatibility alias for <b>normalize</b>. </dd>
<dt> <b>reject</b> </dt> <dd> Require the standard End-of-DATA
Such clients
can be excluded with smtpd_forbid_bare_newline_exclusions.
.br
+.IP "\fBnote\fR"
+Same as "normalize", but also notes in
+the log whether the Postfix SMTP server received any lines with
+"bare <LF>". The information is formatted as "disconnect
+from name[address] ... notes=bare_lf". The new value is
+expected to become a list of comma\-separated names.
+.br
+.br
+This
+feature is available in Postfix 3.9 and later.
+.br
.IP "\fByes\fR"
Compatibility alias for \fBnormalize\fR.
.br
--- /dev/null
+#!/bin/sh
+
+version=$(basename $(env - pwd)) || exit 1
+case "$version" in
+postfix-[0-9]*.[0-9]*.[0-9]*)
+ test -f conf/makedefs.out || {
+ echo "Error: no conf/makedefs.out" 1>&2; exit 1; }
+ grep 'CCARGS.*-DSNAPSHOT' conf/makedefs.out && {
+ echo "Error: stable release builds with -DSNAPSHOT" 1>&2, exit 1; }
+ grep 'CCARGS.*-DNONPROD' conf/makedefs.out && {
+ echo "Error: stable release builds with -DNONPROD" 1>&2, exit 1; }
+ ;;
+esac
<CR><LF>.<CR><LF>. <br> <br> Such clients
can be excluded with smtpd_forbid_bare_newline_exclusions. </dd>
+<dt> <b>note</b> </dt> <dd> Same as "normalize", but also notes in
+the log whether the Postfix SMTP server received any lines with
+"bare <LF>". The information is formatted as "<tt>disconnect
+from name[address] ... notes=bare_lf</tt>". The new value is
+expected to become a list of comma-separated names. <br> <br> This
+feature is available in Postfix 3.9 and later. </dd>
+
<dt> <b>yes</b> </dt> <dd> Compatibility alias for <b>normalize</b>. </dd>
<dt> <b>reject</b> </dt> <dd> Require the standard End-of-DATA
stable releases Files global smtp_stream hc smtpd smtpd c
Files global smtp_stream hc smtpd smtpd c
Files smtpd smtpd c proto postconf proto
+ names Files smtpd smtpd hc
+ or access control limitations Files smtpd smtpd hc
+ spam the log with a warning message File tlsmgr tlsmgr c
2045 Sections 2 7 and 2 8 br br Such clients can be excluded
br br This will also reject email from services that use BDAT
RFC 2045 Sections 2 7 and 2 8 br br Such clients can be
+to become a list of comma separated names br br This feature
Levente
MariaDB
dehtml
+NONPROD
* Patches change both the patchlevel and the release date. Snapshots have no
* patchlevel; they change the release date only.
*/
-#define MAIL_RELEASE_DATE "20240121"
+#define MAIL_RELEASE_DATE "20240124"
#define MAIL_VERSION_NUMBER "3.9"
#ifdef SNAPSHOT
*/
#define BARE_LF_FLAG_WANT_STD_EOD (1<<0) /* Require CRLF.CRLF */
#define BARE_LF_FLAG_REPLY_REJECT (1<<1) /* Reject bare newline */
+#define BARE_LF_FLAG_NOTE_LOG (1<<2) /* Note bare newline */
#define IS_BARE_LF_WANT_STD_EOD(m) ((m) & BARE_LF_FLAG_WANT_STD_EOD)
#define IS_BARE_LF_REPLY_REJECT(m) ((m) & BARE_LF_FLAG_REPLY_REJECT)
+#define IS_BARE_LF_NOTE_LOG(m) ((m) & BARE_LF_FLAG_NOTE_LOG)
static const NAME_CODE bare_lf_mask_table[] = {
"normalize", BARE_LF_FLAG_WANT_STD_EOD, /* Default */
"yes", BARE_LF_FLAG_WANT_STD_EOD, /* Migration aid */
+ "note", BARE_LF_FLAG_WANT_STD_EOD | BARE_LF_FLAG_NOTE_LOG,
"reject", BARE_LF_FLAG_WANT_STD_EOD | BARE_LF_FLAG_REPLY_REJECT,
"no", 0,
0, -1, /* error */
curr_rec_type = REC_TYPE_CONT;
if (IS_BARE_LF_REPLY_REJECT(smtp_got_bare_lf))
state->err |= CLEANUP_STAT_BARE_LF;
+ else if (IS_BARE_LF_NOTE_LOG(smtp_got_bare_lf))
+ state->notes |= SMTPD_NOTE_BARE_LF;
start = vstring_str(state->buffer);
len = VSTRING_LEN(state->buffer);
if (first) {
/*
* Read lines from the fragment. The last line may continue in the
* next fragment, or in the next chunk.
+ *
+ * If smtp_get_noexcept() stopped after var_line_limit bytes and did not
+ * emit a queue file record, then that means smtp_get_noexcept()
+ * stopped after CR and hit EOF as it tried to find out if the next
+ * byte is LF. In that case, read the first byte from the next
+ * fragment or chunk, and if that first byte is LF, then
+ * smtp_get_noexcept() strips off the trailing CRLF and returns '\n'
+ * as it always does after reading a complete line.
*/
do {
+ int can_read = var_line_limit - LEN(state->bdat_get_buffer);
+
if (smtp_get_noexcept(state->bdat_get_buffer,
state->bdat_get_stream,
- var_line_limit,
+ can_read > 0 ? can_read : 1, /* Peek one */
SMTP_GET_FLAG_APPEND) == '\n') {
/* Stopped at end-of-line. */
curr_rec_type = REC_TYPE_NORM;
+ } else if (LEN(state->bdat_get_buffer) > var_line_limit) {
+ /* Undo peeking, and output the buffer as REC_TYPE_CONT. */
+ vstream_ungetc(state->bdat_get_stream,
+ vstring_end(state->bdat_get_buffer)[-1]);
+ vstring_truncate(state->bdat_get_buffer,
+ LEN(state->bdat_get_buffer) - 1);
+ curr_rec_type = REC_TYPE_CONT;
} else if (!vstream_feof(state->bdat_get_stream)) {
/* Stopped at var_line_limit. */
curr_rec_type = REC_TYPE_CONT;
}
if (IS_BARE_LF_REPLY_REJECT(smtp_got_bare_lf))
state->err |= CLEANUP_STAT_BARE_LF;
+ else if (IS_BARE_LF_NOTE_LOG(smtp_got_bare_lf))
+ state->notes |= SMTPD_NOTE_BARE_LF;
start = vstring_str(state->bdat_get_buffer);
len = VSTRING_LEN(state->bdat_get_buffer);
if (state->err == CLEANUP_STAT_OK) {
#endif
-#if !defined(USE_TLS) || !defined(USE_SASL_AUTH)
-
/* unimpl_cmd - dummy for functionality that is not compiled in */
static int unimpl_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *unused_argv)
return (-1);
}
-#endif
-
/*
* The table of all SMTP commands that we know. Set the junk limit flag on
* any command that can be repeated an arbitrary number of times without
#define SMTPD_CMD_FLAG_PRE_TLS (1<<1) /* allow before STARTTLS */
#define SMTPD_CMD_FLAG_LAST (1<<2) /* last in PIPELINING command group */
+static int help_cmd(SMTPD_STATE *, int, SMTPD_TOKEN *);
+
static SMTPD_CMD smtpd_cmd_table[] = {
{SMTPD_CMD_HELO, helo_cmd, SMTPD_CMD_FLAG_LIMIT | SMTPD_CMD_FLAG_PRE_TLS | SMTPD_CMD_FLAG_LAST,},
{SMTPD_CMD_EHLO, ehlo_cmd, SMTPD_CMD_FLAG_LIMIT | SMTPD_CMD_FLAG_PRE_TLS | SMTPD_CMD_FLAG_LAST,},
{SMTPD_CMD_VRFY, vrfy_cmd, SMTPD_CMD_FLAG_LIMIT | SMTPD_CMD_FLAG_LAST,},
{SMTPD_CMD_ETRN, etrn_cmd, SMTPD_CMD_FLAG_LIMIT,},
{SMTPD_CMD_QUIT, quit_cmd, SMTPD_CMD_FLAG_PRE_TLS,},
+ {SMTPD_CMD_HELP, help_cmd, SMTPD_CMD_FLAG_PRE_TLS,},
{0,},
};
static STRING_LIST *smtpd_noop_cmds;
static STRING_LIST *smtpd_forbid_cmds;
+/* help_cmd - process HELP command */
+
+static int help_cmd(SMTPD_STATE *state, int unused_argc, SMTPD_TOKEN *unused_argv)
+{
+ ARGV *argv = argv_alloc(sizeof(smtpd_cmd_table)
+ / sizeof(*smtpd_cmd_table));
+ VSTRING *buf = vstring_alloc(100);
+ SMTPD_CMD *cmdp;
+
+ /*
+ * Return a list of implemented commands.
+ *
+ * The HELP command does not suppress commands that can be dynamically
+ * disabled in the EHLO response or through access control. That would
+ * require refactoring the EHLO feature-suppression and per-feature
+ * access control, so that they can be reused (not duplicated) here.
+ *
+ * The HELP command does not provide information that makes Postfix easier
+ * to fingerprint, such as software name, version, or build information.
+ */
+ for (cmdp = smtpd_cmd_table; cmdp->name != 0; cmdp++)
+ if (cmdp->action != unimpl_cmd)
+ argv_add(argv, cmdp->name, ARGV_END);
+ argv_sort(argv);
+ smtpd_chat_reply(state, "214 2.0.0 Commands: %s",
+ argv_join(buf, argv, ' '));
+ vstring_free(buf);
+ argv_free(argv);
+ return (0);
+}
+
/* smtpd_flag_ill_pipelining - flag pipelining protocol violation */
static int smtpd_flag_ill_pipelining(SMTPD_STATE *state)
var_smtpd_forbid_bare_lf_code, var_myhostname);
break;
}
+ if (IS_BARE_LF_NOTE_LOG(smtp_got_bare_lf))
+ state->notes |= SMTPD_NOTE_BARE_LF;
/* Safety: protect internal interfaces against malformed UTF-8. */
if (var_smtputf8_enable
&& valid_utf8_stringz(STR(state->buffer)) == 0) {
/* smtpd_format_cmd_stats - format per-command statistics */
-static char *smtpd_format_cmd_stats(VSTRING *buf)
+static char *smtpd_format_cmd_stats(SMTPD_STATE *state)
{
SMTPD_CMD *cmdp;
int all_success = 0;
int all_total = 0;
+ VSTRING *buf = state->buffer;
/*
* Log the statistics. Note that this loop produces no output when no
vstring_sprintf_append(buf, " commands=%d", all_success);
if (all_success != all_total || all_total == 0)
vstring_sprintf_append(buf, "/%d", all_total);
+
+ /*
+ * Log aggregated warnings.
+ */
+ if (state->notes & SMTPD_NOTE_BARE_LF)
+ vstring_sprintf_append(buf, " notes=bare_lf");
+
return (lowercase(STR(buf)));
}
* connection time.
*/
msg_info("disconnect from %s%s", state.namaddr,
- smtpd_format_cmd_stats(state.buffer));
+ smtpd_format_cmd_stats(&state));
teardown_milters(&state); /* duplicates xclient_cmd */
smtpd_state_reset(&state);
debug_peer_restore();
int junk_cmds; /* counter */
int rcpt_overshoot; /* counter */
char *rewrite_context; /* address rewriting context */
+ int notes; /* notes aggregator */
/*
* SASL specific.
#define SMTPD_FLAG_SMTPUTF8 (1<<3) /* RFC 6531/2 transaction */
#define SMTPD_FLAG_NEED_MILTER_ABORT (1<<4) /* undo milter_mail_event() */
+#define SMTPD_NOTE_BARE_LF (1<<0) /* saw at least one bare LF */
+
/* Security: don't reset SMTPD_FLAG_AUTH_USED. */
#define SMTPD_MASK_MAIL_KEEP \
~(SMTPD_FLAG_SMTPUTF8) /* Fix 20140706 */
#define SMTPD_CMD_XCLIENT "XCLIENT"
#define SMTPD_CMD_XFORWARD "XFORWARD"
#define SMTPD_CMD_UNKNOWN "UNKNOWN"
+#define SMTPD_CMD_HELP "HELP"
/*
* Representation of unknown and non-existent client information. Throughout
state->instance = vstring_alloc(10);
state->seqno = 0;
state->rewrite_context = 0;
+ state->notes = 0;
#if 0
state->ehlo_discard_mask = ~0;
#else
ATTR_TYPE_END);
}
vstream_fflush(client_stream);
+
+ /*
+ * Reportedly, some OS lies under load; it tells the Postfix event
+ * handler that a server socket is readable, then it tells peekfd() that
+ * the socket has pending data, and then it tells vstring_get_null() that
+ * there is none, causing Postfix to spam the log with warning messages.
+ * Close the stream to stop such nonsense; the client can reconnect if it
+ * still wants to talk to us.
+ *
+ * XXX Why is this problem not reported for the other five
+ * multi_server-based Postfix services?
+ */
+ if (vstream_ferror(client_stream) || vstream_feof(client_stream))
+ multi_server_disconnect(client_stream);
+ /* Note: client_stream is now a dangling pointer. */
}
/* tlsmgr_pre_init - pre-jail initialization */
allspace.o: vstring.h
argv.o: argv.c
argv.o: argv.h
+argv.o: check_arg.h
argv.o: msg.h
argv.o: mymalloc.h
argv.o: sys_defs.h
+argv.o: vbuf.h
+argv.o: vstring.h
argv_attr_print.o: argv.h
argv_attr_print.o: argv_attr.h
argv_attr_print.o: argv_attr_print.c
/* ssize_t pos;
/* ssize_t how_many;
/*
+/* char *argv_join(buf, argvp, delim)
+/* VSTRING *buf;
+/* ARGV *argvp;
+/* int delim;
+/*
/* void ARGV_FAKE_BEGIN(argv, arg)
/* const char *arg;
/*
/* starting at the specified array position. The result is
/* null-terminated.
/*
+/* argv_join() joins all elements in an array using the
+/* specified delimiter value, and appends the result to the
+/* specified buffer.
+/*
/* ARGV_FAKE_BEGIN/END are an optimization for the case where
/* a single string needs to be passed into an ARGV-based
/* interface. ARGV_FAKE_BEGIN() opens a statement block and
#include "mymalloc.h"
#include "msg.h"
+#include "vstring.h"
#include "argv.h"
#ifdef TEST
argvp->argc -= how_many;
}
+/* argv_join - concatenate array elements with delimiter */
+
+char *argv_join(VSTRING *buf, ARGV *argv, int delim)
+{
+ char **cpp;
+
+ for (cpp = argv->argv; *cpp; cpp++) {
+ vstring_strcat(buf, *cpp);
+ if (cpp[1])
+ VSTRING_ADDCH(buf, delim);
+ }
+ return (vstring_str(buf));
+}
+
#ifdef TEST
/*
const char *exp_panic_msg; /* expected panic */
int exp_argc; /* expected array length */
const char *exp_argv[ARRAY_LEN]; /* expected array content */
+ int join_delim; /* argv_join() delimiter */
} TEST_CASE;
#define TERMINATE_ARRAY (1)
return (argvp);
}
+/* test_argv_join - populate, join, and overwrite */
+
+static ARGV *test_argv_join(const TEST_CASE *tp, ARGV *argvp)
+{
+ VSTRING *buf = vstring_alloc(100);
+
+ /*
+ * Impedance mismatch: argv_join() produces output to VSTRING, but the
+ * test fixture wants output to ARGV.
+ */
+ test_argv_populate(tp, argvp);
+ argv_join(buf, argvp, tp->join_delim);
+ argv_delete(argvp, 0, argvp->argc);
+ argv_add(argvp, vstring_str(buf), ARGV_END);
+ vstring_free(buf);
+ return (argvp);
+}
+
/* test_argv_verify - verify result */
static int test_argv_verify(const TEST_CASE *tp, ARGV *argvp)
}
if (strcmp(vstring_str(test_panic_str), tp->exp_panic_msg) != 0) {
msg_warn("test case '%s': got '%s', want: '%s'",
- tp->label, vstring_str(test_panic_str), tp->exp_panic_msg);
+ tp->label, vstring_str(test_panic_str), tp->exp_panic_msg);
return (FAIL);
}
return (PASS);
{"foo", "baz", "bar", 0}, 0, test_argv_bad_delete3,
"argv_delete bad range: (start=100 count=1)"
},
+ {"argv_join, multiple strings",
+ {"foo", "baz", "bar", 0}, 0, test_argv_join,
+ 0, 1, {"foo:baz:bar", 0}, ':'
+ },
+ {"argv_join, one string",
+ {"foo", 0}, 0, test_argv_join,
+ 0, 1, {"foo", 0}, ':'
+ },
+ {"argv_join, empty",
+ {0}, 0, test_argv_join,
+ 0, 1, {"", 0}, ':'
+ },
0,
};
extern void argv_insert_one(ARGV *, ssize_t, const char *);
extern void argv_replace_one(ARGV *, ssize_t, const char *);
extern void argv_delete(ARGV *, ssize_t, ssize_t);
+struct VSTRING;
+extern char *argv_join(struct VSTRING *buf, ARGV *, int);
extern ARGV *argv_free(ARGV *);
extern ARGV *argv_split(const char *, const char *);