[ Reason ]
There's a new upstream stable/bugfix/security release,
including a fixes for a few security issues too.
[ Tests ]
It works in our environment for quite some time already.
[ Risks ]
As it is usual with postfix releases, it's safe to
perform the update. All the changes are small, easy
to understand and verify. I see no potential for any
breakage here.
[ Checklist ]
[X] *all* changes are documented in the d/changelog
[X] I reviewed all changes and I approve them
[X] attach debdiff against the package in (old)stable
[X] the issue is verified as fixed in unstable
[ Changes ]
The changelog is in the debdiff (d/changelog).
Additionally, there's the upstream announcement,
https://www.postfix.org/announcements/postfix-3.11.7.html
The debdiff is below.
Thanks,
/mjt
diff -Nru postfix-3.10.13/HISTORY postfix-3.10.14/HISTORY
--- postfix-3.10.13/HISTORY 2026-08-09 23:47:07.000000000 +0300
+++ postfix-3.10.14/HISTORY 2026-09-06 17:10:47.000000000 +0300
@@ -29766,3 +29766,103 @@
Queue hygiene: the postdrop command accepted the null record
type which the rest of Postfix ingnores. Reported by OpenAI
Security. File: postdrop.c.
+
+20260903
+
+ Bug (defect introduced: Postfix 2.3, date: 20050323): the
+ SMTP client enhanced status code parser could process stale
+ data. Reported by Qualys, assisted by Claude Mythos Preview.
+ File: smtp_chat.c.
+
+ Bug (defect introduced: Postfix 3.0, date: 20141117): don't
+ free() text before logging a fatal error message. Reported
+ by Qualys, assisted by Claude Mythos Preview. File:
+ postqueue.c.
+
+ Bug (defect introduced: Postfix-beta, date: 19990119): the
+ pipe(8) delivery agent deleted a command-line argument if
+ the argument contained $user AND $user expanded to an empty
+ string, breaking the positional order of arguments. This
+ was a workaround for a problem that hopefully no longer
+ exists. Reported by Qualys, assisted by Claude Mythos
+ Preview. File: pipe/pipe.c.
+
+ Code hygiene: in the SMTP client protocol engine, evaluate
+ the RETURN() macro argument before freeing resources.
+ Reported by Qualys, assisted by Claude Mythos Preview. File:
+ smtp_proto.c.
+
+ Code hygiene: myrealloc(ptr, 0) still resulted in a panic.
+ Reported by Qualys, assisted by Claude Mythos Preview. Also
+ adopt a mystrndup() fix from Postfix 3.12. File: mymalloc.c.
+
+ The postmap and postalias commands now log a warning when
+ creating a root-owned database file in a directory that is
+ not owned by root. They log that the database source file,
+ indexed file(s), and parent directory should have the same
+ owner, to prevent a privilege-escalation attack. Problem
+ reported by OpenAI Security, remediation strategy (don't
+ break PROD) by Wietse. Files: postalias.c, postmap.c.
+
+ Hardening command-line email submission: the postdrop
+ command now disallows null and line-break characters in
+ queue file envelope records (line-break characters in other
+ queue file records are already neutralized by default with
+ "cleanup_replace_stray_cr_lf = yes").
+
+ The new constraint not only eliminates line-break injection
+ into local mailbox files as reported by OpenAI Security,
+ but also prevents other forms of misuse. Later, this
+ constraint may be moved into the Postfix core. Fix by Wietse.
+ File: postdrop.c.
+
+ Shut up nagging from multiple AIs and harden the virtual
+ delivery agent against an evil SQL database. File:
+ virtual/mailbox.c.
+
+ Bug (defect introduced: Postfix 3.4, date: 20180303): the
+ MySQL client setting "tls_verify_cert = yes" had no effect
+ with Oracle MySQL 8 and later. Report and fix by OpenAI
+ Security. File: dict_mysql.c.
+
+ Isolation: stamp Postfix SMTP server TLS session tickets
+ with their master.cf service name. With this, an SMTP server
+ defined in master.cf will no longer accept tickets issued
+ by a different SMTP server defined in the same master.cf
+ file. Fix by OpenAI security. File: tls_server.c.
+
+ Bug (introduced: Postfix 2.3, date: 20060629): a malicious
+ Milter or attacker-in-the-middle could trigger a null-terminated
+ heap memory overread in the SMTP daemon while formatting a
+ malformed multiline response. Fix from OpenAI Security
+ adopted with minor changes. Files: milter8.c, smtpd_chat.c.
+
+ Bug (introduced: Postfix 3.9, date: 20240106) SMTP smuggling
+ was still possible with smtpd_proxy_filter (disabled by
+ default) when the after-filter SMTP server used the default
+ policy settings "smtpd_forbid_bare_newline_exclusions =
+ $mynetworks" and "smtpd_forbid_bare_newline = normalize".
+ Reported by OpenAI Security. Fix by Wietse. Files:
+ smtpd_proxy.[hc].
+
+ Cleanup: as suggested by OpenAI Security, eliminate stray
+ CR characters from the smtpd_proxy_filter input stream.
+ The before-proxy-filter SMTP server already eliminated stray
+ LF. File: smtpd/smtpd_proxy.[hc].
+
+ Portability: OpenBSD does not define NS_INT16SZ. Brad Smith.
+ File: dns.h.
+
+ Shut up nagging about allowing gid==0 in database responses.
+ File: virtual/mailbox.c.
+
+ Bug (defect introduced: Postfix 3.0, date: 20140707): null
+ pointer read error after receiving MAIL FROM, RCPT TO, and
+ VRFY with an UTF8 address but no SMTPUTF8 parameter. This
+ requires "smtputf8_enable = yes" (the default) and
+ "strict_smtputf8 = yes" (not default). With this, the SMTP
+ server did an unnecessary MAIL FROM reset without RCPT TO
+ reset. A crafted remote SMTP client could then send a DATA
+ command and crash the Postfix SMTP server with a null pointer
+ read error. Reported by Wonyoung Jung (정원영). File:
+ smtpd/smtpd.c.
diff -Nru postfix-3.10.13/debian/changelog postfix-3.10.14/debian/changelog
--- postfix-3.10.13/debian/changelog 2026-08-10 19:35:04.000000000 +0300
+++ postfix-3.10.14/debian/changelog 2026-09-08 15:31:38.000000000 +0300
@@ -1,3 +1,100 @@
+postfix (3.10.14-0+deb13u1) trixie; urgency=medium
+
+ * new upstream stable/bugfix/security release
+ From the release announcement by Wietse Wenema at
+ https://www.postfix.org/announcements/postfix-3.11.7.html :
+
+ These defects were found by "Qualys assisted by Claude Mythos Preview",
+ and by "OpenAI Security"; three date from 20 or more years ago.
+
+ o SMTP smuggling:
+
+ - Bug (introduced: Postfix 3.9, date: 20240106) SMTP smuggling was
+ still possible with smtpd_proxy_filter (disabled by default) when
+ the after-filter SMTP server used the default policy settings
+ "smtpd_forbid_bare_newline_exclusions = $mynetworks" and
+ "smtpd_forbid_bare_newline = normalize".
+
+ - Bug (introduced: Postfix 3.11, date: 20250917): SMTP smuggling was
+ possible with smtpd_proxy_filter (disabled by default) when the
+ before-filter SMTP server added a "Require-TLS-ESMTP: yes" message
+ header, due to implementation edge cases. Adding this header is
+ enabled with the "requiretls_esmtp_header = yes" default setting.
+
+ o Server crashes and panic()s:
+
+ - Bug (defect introduced: Postfix 3.0, date: 20140707): null pointer read
+ error after receiving MAIL FROM, RCPT TO, and VRFY with an UTF8 address
+ but no SMTPUTF8 parameter. This requires "smtputf8_enable = yes" (the
+ default) and "strict_smtputf8 = yes" (not default). With this, the SMTP
+ server did an unnecessary MAIL FROM reset without RCPT TO reset.
+ A crafted remote SMTP client could then send a DATA command and crash a
+ Postfix SMTP server process with a null pointer read error.
+
+ o Other bugs:
+
+ - Bug (defect introduced: Postfix 3.4, date: 20180303): the MySQL
+ client setting "tls_verify_cert = yes" had no effect with Oracle
+ MySQL 8 and later.
+
+ - Bug (defect introduced: Postfix-beta, date: 19990119): the pipe(8)
+ delivery agent deleted a command-line argument if the argument contained
+ $user AND $user expanded to an empty string, breaking the positional
+ order of arguments. This was a workaround for a problem that hopefully
+ no longer exists.
+
+ - Bug (defect introduced: Postfix 2.3, date: 20050323): the SMTP client
+ enhanced status code parser could process stale data when a remote SMTP
+ server sent a three-digit reply without other text.
+
+ o TLS:
+
+ - Isolation: stamp Postfix SMTP server TLS session tickets with their
+ master.cf service name. With this, an SMTP server defined in master.cf
+ will no longer accept tickets issued by a different SMTP server defined
+ in the same master.cf file.
+
+ o Configuration safety:
+
+ - The postmap and postalias commands now log a warning when creating a
+ root-owned database file in a directory that is not owned by root. They
+ log that the database source file, indexed file(s), and parent directory
+ should have the same owner, to prevent a privilege-escalation attack.
+
+ o Read after free, memory over-read:
+
+ - Bug (introduced: Postfix 2.3, date: 20060629): a malicious Milter or
+ attacker-in-the-middle could trigger a null-terminated heap memory
+ overread in the SMTP daemon while formatting a malformed multiline
+ response.
+
+ - Bug (defect introduced: Postfix 3.0, date: 20141117): in the postqueue
+ command don't free() text before logging a fatal error message.
+
+ - Code hygiene: in the SMTP client protocol engine, evaluate a RETURN()
+ macro argument before freeing resources.
+
+ o Code hardening (defense in depth, prevention):
+
+ - Hardening command-line email submission: the postdrop command now
+ disallows null and line-break characters in queue file envelope records
+ (line-break characters in non-envelope queue file records are already
+ neutralized by default with "cleanup_replace_stray_cr_lf = yes").
+
+ - The new constraint not only eliminates line-break injection into local
+ mailbox files, but also prevents other forms of misuse. Later, this
+ constraint may be moved into the Postfix core.
+
+ - Shut up nagging from multiple AIs and harden the virtual delivery agent
+ against an evil (LDAP or SQL) database.
+
+ - Code hygiene: myrealloc(ptr, 0) still resulted in a panic.
+ Also adopt a mystrndup() fix from Postfix 3.12.
+
+ * debian-defaults.patch: fix typo (inet_protocools) (Closes: #1144900)
+
+ -- Michael Tokarev <mjt@tls.msk.ru> Tue, 08 Sep 2026 15:31:38 +0300
+
postfix (3.10.13-0+deb13u1) trixie-security; urgency=medium
* new upstream stable/bugfix/security release
diff -Nru postfix-3.10.13/debian/patches/debian-defaults.patch postfix-3.10.14/debian/patches/debian-defaults.patch
--- postfix-3.10.13/debian/patches/debian-defaults.patch 2026-08-10 19:08:30.000000000 +0300
+++ postfix-3.10.14/debian/patches/debian-defaults.patch 2026-09-08 15:11:57.000000000 +0300
@@ -41,7 +41,7 @@
+# separated by whitespace or commas. The form "all" is equivalent
+# to "ipv4, ipv6".
+#
-+#inet_protocools = ipv4
++#inet_protocols = ipv4
+inet_protocols = all
+
# INTERNET HOST AND DOMAIN NAMES
diff -Nru postfix-3.10.13/src/dns/dns.h postfix-3.10.14/src/dns/dns.h
--- postfix-3.10.13/src/dns/dns.h 2024-11-21 20:49:05.000000000 +0300
+++ postfix-3.10.14/src/dns/dns.h 2026-09-04 00:12:22.000000000 +0300
@@ -54,6 +54,10 @@
#endif
+#ifndef NS_INT16SZ
+#define NS_INT16SZ 2
+#endif
+
/*
* Provide API compatibility for systems without res_nxxx() API. Also
* require calling dns_get_h_errno() instead of directly accessing the
diff -Nru postfix-3.10.13/src/global/dict_mysql.c postfix-3.10.14/src/global/dict_mysql.c
--- postfix-3.10.13/src/global/dict_mysql.c 2025-02-07 18:36:50.000000000 +0300
+++ postfix-3.10.14/src/global/dict_mysql.c 2026-09-04 00:12:22.000000000 +0300
@@ -118,7 +118,8 @@
#if defined(MARIADB_BASE_VERSION) && MYSQL_VERSION_ID >= 50023
#define DICT_MYSQL_SSL_VERIFY_SERVER_CERT MYSQL_OPT_SSL_VERIFY_SERVER_CERT
#elif MYSQL_VERSION_ID >= 80000
-#define DICT_MYSQL_SSL_VERIFY_SERVER_CERT MYSQL_OPT_SSL_MODE
+/* 202607 OpenAI: MYSQL_OPT_SSL_MODE is an enum, not a bool. */
+#define DICT_MYSQL_SSL_MODE MYSQL_OPT_SSL_MODE
#endif
/*
@@ -171,7 +172,7 @@
char *tls_CAfile;
char *tls_CApath;
char *tls_ciphers;
-#if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT)
+#if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT) || defined(DICT_MYSQL_SSL_MODE)
int tls_verify_cert;
#endif
int require_result_set;
@@ -601,6 +602,11 @@
*/
static void plmysql_connect_single(DICT_MYSQL *dict_mysql, HOST *host)
{
+#if defined(DICT_MYSQL_SSL_MODE)
+ enum mysql_ssl_mode ssl_mode;
+
+#endif
+
if ((host->db = mysql_init(NULL)) == NULL)
msg_fatal("dict_mysql: insufficient memory");
if (dict_mysql->option_file)
@@ -628,7 +634,19 @@
dict_mysql->tls_CAfile, dict_mysql->tls_CApath,
dict_mysql->tls_ciphers);
#endif
-#if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT)
+#if defined(DICT_MYSQL_SSL_MODE)
+ if (dict_mysql->tls_verify_cert != -1) {
+ /* MYSQL_OPT_SSL_MODE enum values are not Boolean values. */
+ ssl_mode = dict_mysql->tls_verify_cert ? SSL_MODE_VERIFY_IDENTITY :
+ SSL_MODE_PREFERRED;
+ if (mysql_options(host->db, DICT_MYSQL_SSL_MODE, &ssl_mode) != 0) {
+ msg_warn("dict_mysql: could not set TLS mode for host %s: %s",
+ host->hostname, mysql_error(host->db));
+ plmysql_down_host(host, dict_mysql->retry_interval);
+ return;
+ }
+ }
+#elif defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT)
if (dict_mysql->tls_verify_cert != -1)
mysql_options(host->db, DICT_MYSQL_SSL_VERIFY_SERVER_CERT,
&dict_mysql->tls_verify_cert);
@@ -704,7 +722,7 @@
dict_mysql->tls_CAfile = cfg_get_str(p, "tls_CAfile", NULL, 0, 0);
dict_mysql->tls_CApath = cfg_get_str(p, "tls_CApath", NULL, 0, 0);
dict_mysql->tls_ciphers = cfg_get_str(p, "tls_ciphers", NULL, 0, 0);
-#if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT)
+#if defined(DICT_MYSQL_SSL_VERIFY_SERVER_CERT) || defined(DICT_MYSQL_SSL_MODE)
dict_mysql->tls_verify_cert = cfg_get_bool(p, "tls_verify_cert", -1);
#endif
dict_mysql->require_result_set = cfg_get_bool(p, "require_result_set", 1);
diff -Nru postfix-3.10.13/src/global/mail_version.h postfix-3.10.14/src/global/mail_version.h
--- postfix-3.10.13/src/global/mail_version.h 2026-08-09 23:47:52.000000000 +0300
+++ postfix-3.10.14/src/global/mail_version.h 2026-09-07 19:24:23.000000000 +0300
@@ -20,8 +20,8 @@
* Patches change both the patchlevel and the release date. Snapshots have no
* patchlevel; they change the release date only.
*/
-#define MAIL_RELEASE_DATE "20260809"
-#define MAIL_VERSION_NUMBER "3.10.13"
+#define MAIL_RELEASE_DATE "20260907"
+#define MAIL_VERSION_NUMBER "3.10.14"
#ifdef SNAPSHOT
#define MAIL_VERSION_DATE "-" MAIL_RELEASE_DATE
diff -Nru postfix-3.10.13/src/milter/milter8.c postfix-3.10.14/src/milter/milter8.c
--- postfix-3.10.13/src/milter/milter8.c 2026-07-06 02:29:02.000000000 +0300
+++ postfix-3.10.14/src/milter/milter8.c 2026-09-04 00:12:22.000000000 +0300
@@ -955,6 +955,64 @@
return (err);
}
+/* milter8_valid_smtp_reply - validate SMTP reply (after 202607 OpenAI) */
+
+static int milter8_valid_smtp_reply(VSTRING *buf)
+{
+ const unsigned char *cp = (unsigned char *) STR(buf);
+ const unsigned char *buf_end = cp + LEN(buf);
+
+ /* Exclude one terminal Milter protocol null from the reply text. */
+ if (buf_end > cp && buf_end[-1] == 0)
+ buf_end -= 1;
+
+ if (memchr(cp, 0, buf_end - cp) != 0)
+ return (0);
+
+ while (cp < buf_end) {
+ const unsigned char *line_end;
+ ssize_t line_len;
+ int final;
+
+ /* Part of a breaking change, not allowed in stable release. */
+#ifdef SNAPSHOT
+ unsigned char code[3];
+ int first = 1;
+
+#endif
+
+ /* A final line must not end in a line terminator. */
+ line_len = strcspn((const char *) cp, "\r\n");
+ line_end = cp + line_len;
+ final = (line_end == buf_end);
+
+ /* Each line must match /^ddd(-| )d?/. */
+ if (line_len < 4 || (cp[0] != '4' && cp[0] != '5') || !ISDIGIT(cp[1])
+ || !ISDIGIT(cp[2]) || (cp[3] != (final ? ' ' : '-'))
+ || (line_len > 4 && ISDIGIT(cp[4]) && cp[4] != cp[0]))
+ return (0);
+
+ /* Part of a breaking change, not allowed in stable release. */
+#ifdef SNAPSHOT
+ if (first) {
+ code[0] = cp[0];
+ code[1] = cp[1];
+ code[2] = cp[2];
+ first = 0;
+ } else if (cp[0] != code[0] || cp[1] != code[1] || cp[2] != code[2]) {
+ return (0);
+ }
+#endif
+ /* A non-final line must end in a line terminator. */
+ if (final)
+ return (1);
+ if (line_end > buf_end - 2 || *line_end != '\r' || line_end[1] != '\n')
+ return (0);
+ cp = line_end + 2;
+ }
+ return (0);
+}
+
/* milter8_event - report event and receive reply */
static const char *milter8_event(MILTER8 *milter, int event,
@@ -1290,13 +1348,7 @@
MILTER8_DATA_BUFFER, milter->buf,
MILTER8_DATA_END) != 0)
MILTER8_EVENT_BREAK(milter->def_reply);
- /* XXX Enforce this for each line of a multi-line reply. */
- if ((STR(milter->buf)[0] != '4' && STR(milter->buf)[0] != '5')
- || !ISDIGIT(STR(milter->buf)[1])
- || !ISDIGIT(STR(milter->buf)[2])
- || (STR(milter->buf)[3] != ' ' && STR(milter->buf)[3] != '-')
- || (ISDIGIT(STR(milter->buf)[4])
- && (STR(milter->buf)[4] != STR(milter->buf)[0]))) {
+ if (!milter8_valid_smtp_reply(milter->buf)) {
msg_warn("milter %s: malformed reply: %s",
milter->m.name, STR(milter->buf));
milter8_conf_error(milter);
diff -Nru postfix-3.10.13/src/pipe/pipe.c postfix-3.10.14/src/pipe/pipe.c
--- postfix-3.10.13/src/pipe/pipe.c 2025-02-02 00:50:29.000000000 +0300
+++ postfix-3.10.14/src/pipe/pipe.c 2026-09-04 00:12:43.000000000 +0300
@@ -765,12 +765,6 @@
* Either anything to the left of the extension delimiter or,
* in absence of the latter, anything to the left of the
* rightmost @.
- *
- * Beware: if the user name is blank (e.g. +user@host), the
- * argument is suppressed. This is necessary to allow for
- * cyrus bulletin-board (global mailbox) delivery. XXX But,
- * skipping empty user parts will also prevent other
- * expansions of this specific command-line argument.
*/
if (state.expand_flag & PIPE_FLAG_USER) {
morph_recipient(buf, rcpt_list->info[i].address,
@@ -780,8 +774,7 @@
rcpt_list->info[i].address);
if (*var_rcpt_delim)
split_addr(STR(buf), var_rcpt_delim);
- if (*STR(buf) == 0)
- continue;
+ /* 202607 Qualys+Mythos: don't skip arg if $user is "".*/
dict_update(PIPE_DICT_TABLE, PIPE_DICT_USER, STR(buf));
}
diff -Nru postfix-3.10.13/src/postalias/postalias.c postfix-3.10.14/src/postalias/postalias.c
--- postfix-3.10.13/src/postalias/postalias.c 2024-04-22 16:56:32.000000000 +0300
+++ postfix-3.10.14/src/postalias/postalias.c 2026-09-04 00:12:43.000000000 +0300
@@ -336,11 +336,30 @@
/*
* If running as root, run as the owner of the source file, so that the
* result shows proper ownership, and so that a bug in postalias does not
- * allow privilege escalation.
+ * allow privilege escalation. If the source file is owned by root, but
+ * the parent directory is not, log a warning with suggestions.
*/
- if ((postalias_flags & POSTALIAS_FLAG_AS_OWNER) && getuid() == 0
- && (st.st_uid != geteuid() || st.st_gid != getegid()))
- set_eugid(st.st_uid, st.st_gid);
+ if ((postalias_flags & POSTALIAS_FLAG_AS_OWNER) && getuid() == 0) {
+ if (st.st_uid != 0) {
+ set_eugid(st.st_uid, st.st_gid);
+ } else {
+ char *parent_dir;
+ struct stat parent_st;
+
+ parent_dir = sane_dirname((VSTRING *) 0, path_name);
+ if (stat(parent_dir, &parent_st) < 0)
+ msg_fatal("stat %s: %m", parent_dir);
+ if (parent_st.st_uid != 0) {
+ msg_warn("Creating root-owned database file(s) %s.* "
+ "in non-root-owned directory %s. Files "
+ "%s, %s.*, and directory %s should have "
+ "the same owner. Please fix ownership "
+ "to avoid a privilege-escalation attack.",
+ path_name, parent_dir, VSTREAM_PATH(source_fp),
+ path_name, parent_dir);
+ }
+ }
+ }
/*
* Override the default per-table cache size for DB map (re)builds. We
diff -Nru postfix-3.10.13/src/postdrop/postdrop.c postfix-3.10.14/src/postdrop/postdrop.c
--- postfix-3.10.13/src/postdrop/postdrop.c 2026-08-09 23:47:07.000000000 +0300
+++ postfix-3.10.14/src/postdrop/postdrop.c 2026-09-04 00:12:43.000000000 +0300
@@ -514,6 +514,11 @@
msg_fatal("uid=%ld: malformed input", (long) uid);
if (rec_type == 0 || strchr(*expected, rec_type) == 0)
msg_fatal("uid=%ld: unexpected record type: %d", (long) uid, rec_type);
+ /* 202607 OpenAI: reject line breaks and nulls in envelope content. */
+ if (rec_type != REC_TYPE_NORM && rec_type != REC_TYPE_CONT
+ && strcspn(vstring_str(buf), "\r\n") != VSTRING_LEN(buf))
+ msg_fatal("uid=%ld: null or line break in '%s' record type: %.200s",
+ (long) uid, rec_type_name(rec_type), vstring_str(buf));
if (rec_type == **expected)
expected++;
/* Override time information from the untrusted caller. */
diff -Nru postfix-3.10.13/src/postmap/postmap.c postfix-3.10.14/src/postmap/postmap.c
--- postfix-3.10.13/src/postmap/postmap.c 2023-10-12 18:34:40.000000000 +0300
+++ postfix-3.10.14/src/postmap/postmap.c 2026-09-04 00:12:43.000000000 +0300
@@ -429,11 +429,30 @@
/*
* If running as root, run as the owner of the source file, so that the
* result shows proper ownership, and so that a bug in postmap does not
- * allow privilege escalation.
+ * allow privilege escalation. If the source file is owned by root, but
+ * the parent directory is not, log a warning with suggestions.
*/
- if ((postmap_flags & POSTMAP_FLAG_AS_OWNER) && getuid() == 0
- && (st.st_uid != geteuid() || st.st_gid != getegid()))
- set_eugid(st.st_uid, st.st_gid);
+ if ((postmap_flags & POSTMAP_FLAG_AS_OWNER) && getuid() == 0) {
+ if (st.st_uid != 0) {
+ set_eugid(st.st_uid, st.st_gid);
+ } else {
+ char *parent_dir;
+ struct stat parent_st;
+
+ parent_dir = sane_dirname((VSTRING *) 0, path_name);
+ if (stat(parent_dir, &parent_st) < 0)
+ msg_fatal("stat %s: %m", parent_dir);
+ if (parent_st.st_uid != 0) {
+ msg_warn("Creating root-owned database file(s) %s.* "
+ "in non-root-owned directory %s. Files "
+ "%s, %s.*, and directory %s should have "
+ "the same owner. Please fix ownership "
+ "to avoid a privilege-escalation attack.",
+ path_name, parent_dir, VSTREAM_PATH(source_fp),
+ path_name, parent_dir);
+ }
+ }
+ }
/*
* Override the default per-table cache size for DB map (re)builds. We
diff -Nru postfix-3.10.13/src/postqueue/postqueue.c postfix-3.10.14/src/postqueue/postqueue.c
--- postfix-3.10.13/src/postqueue/postqueue.c 2023-12-04 03:11:58.000000000 +0300
+++ postfix-3.10.14/src/postqueue/postqueue.c 2026-09-04 00:12:43.000000000 +0300
@@ -422,10 +422,11 @@
stat = vstream_pclose(showq);
}
argv_free(argv);
- myfree(showq_path);
+ /* 202606 Qualys+Mythos: don't free() text before logging. */
if (stat != 0)
msg_fatal_status(stat < 0 ? EX_OSERR : EX_SOFTWARE,
"Error running %s", showq_path);
+ myfree(showq_path);
}
/*
diff -Nru postfix-3.10.13/src/smtp/smtp_chat.c postfix-3.10.14/src/smtp/smtp_chat.c
--- postfix-3.10.13/src/smtp/smtp_chat.c 2021-10-02 17:46:46.000000000 +0300
+++ postfix-3.10.14/src/smtp/smtp_chat.c 2026-09-04 00:12:43.000000000 +0300
@@ -413,7 +413,8 @@
if (three_digs != 0) {
rdata.code = atoi(STR(session->buffer));
if (strchr("245", STR(session->buffer)[0]) != 0) {
- for (cp = STR(session->buffer) + 4; *cp == ' '; cp++)
+ /* 202606 Qualys+Mythos: start loop immediately after 'ddd'. */
+ for (cp = STR(session->buffer) + 3; *cp == ' '; cp++)
/* void */ ;
if ((len = dsn_valid(cp)) > 0 && *cp == *STR(session->buffer)) {
vstring_strncpy(rdata.dsn_buf, cp, len);
diff -Nru postfix-3.10.13/src/smtp/smtp_proto.c postfix-3.10.14/src/smtp/smtp_proto.c
--- postfix-3.10.13/src/smtp/smtp_proto.c 2026-06-17 20:06:59.000000000 +0300
+++ postfix-3.10.14/src/smtp/smtp_proto.c 2026-09-04 00:12:43.000000000 +0300
@@ -1573,7 +1573,10 @@
/* Caution: changes to RETURN() also affect code outside the main loop. */
+ /* 202607 Qualys+Mythos: evaluate argument before freeing resources. */
+
#define RETURN(x) do { \
+ int _rv = (x); \
if (recv_state != SMTP_STATE_LAST) \
DONT_CACHE_THIS_SESSION; \
vstring_free(next_command); \
@@ -1581,7 +1584,7 @@
myfree((void *) survivors); \
if (session->mime_state) \
session->mime_state = mime_state_free(session->mime_state); \
- return (x); \
+ return (_rv); \
} while (0)
#define SENDER_IS_AHEAD \
diff -Nru postfix-3.10.13/src/smtpd/smtpd.c postfix-3.10.14/src/smtpd/smtpd.c
--- postfix-3.10.13/src/smtpd/smtpd.c 2026-08-09 23:47:07.000000000 +0300
+++ postfix-3.10.14/src/smtpd/smtpd.c 2026-09-04 00:12:43.000000000 +0300
@@ -4256,8 +4256,8 @@
state->act_size += len + 2;
if (*start == '.' && proxy != 0
&& state->bdat_prev_rec_type != REC_TYPE_CONT)
- if (out_record(out_stream, REC_TYPE_CONT, ".", 1) < 0)
- state->err = out_error;
+ /* 202507 OpenAI: more robust dot-stuffing. */
+ vstring_prepend(state->bdat_get_buffer, ".", 1);
if (state->err == CLEANUP_STAT_OK
&& out_record(out_stream, curr_rec_type,
vstring_str(state->bdat_get_buffer),
@@ -4441,7 +4441,7 @@
/* Fix 20140707: Check the VRFY command. */
if (smtputf8 == 0 && var_strict_smtputf8) {
if (*STR(state->addr_buf) && !allascii(STR(state->addr_buf))) {
- mail_reset(state);
+ /* Wonyoung.Jung 20260811: don't reset SMTP state. */
smtpd_chat_reply(state, "553 5.6.7 Must declare SMTPUTF8 to send unicode address");
return (-1);
}
diff -Nru postfix-3.10.13/src/smtpd/smtpd_chat.c postfix-3.10.14/src/smtpd/smtpd_chat.c
--- postfix-3.10.13/src/smtpd/smtpd_chat.c 2021-10-02 17:46:46.000000000 +0300
+++ postfix-3.10.14/src/smtpd/smtpd_chat.c 2026-09-04 00:12:43.000000000 +0300
@@ -207,6 +207,7 @@
char *next;
char *end;
const char *footer;
+ ssize_t line_len; /* 202607 OpenAI line length checks */
/*
* Slow down clients that make errors. Sleep-on-anything slows down
@@ -226,20 +227,26 @@
/* All 5xx replies must have a 5.xx.xx detail code. */
for (cp = STR(state->buffer), end = cp + strlen(STR(state->buffer));;) {
+ if ((next = strstr(cp, "\r\n")) != 0)
+ line_len = next - cp;
+ else
+ line_len = end - cp;
if (var_soft_bounce) {
- if (cp[0] == '5') {
+ if (line_len > 0 && cp[0] == '5') {
cp[0] = '4';
- if (cp[4] == '5')
+ if (line_len > 4 && cp[4] == '5')
cp[4] = '4';
}
}
/* This is why we use strlen() above instead of VSTRING_LEN(). */
- if ((next = strstr(cp, "\r\n")) != 0) {
+ if (next != 0) {
*next = 0;
- if (next[2] != 0)
- cp[3] = '-'; /* contact footer kludge */
- else
+ if (next[2] != 0) {
+ if (line_len > 3)
+ cp[3] = '-'; /* contact footer kludge */
+ } else {
next = end; /* strip trailing \r\n */
+ }
} else {
next = end;
}
@@ -248,7 +255,7 @@
if (msg_verbose)
msg_info("> %s: %s", state->namaddr, cp);
- smtp_fputs(cp, next - cp, state->client);
+ smtp_fputs(cp, line_len, state->client);
if (next < end)
cp = next + 2;
else
diff -Nru postfix-3.10.13/src/smtpd/smtpd_proxy.c postfix-3.10.14/src/smtpd/smtpd_proxy.c
--- postfix-3.10.13/src/smtpd/smtpd_proxy.c 2022-11-28 23:07:21.000000000 +0300
+++ postfix-3.10.14/src/smtpd/smtpd_proxy.c 2026-09-04 16:45:16.000000000 +0300
@@ -849,12 +849,39 @@
return (rec_type);
}
+/* smtpd_proxy_fix_line_breaks - replace line-break crud with space */
+
+static const char *smtpd_proxy_fix_line_breaks(SMTPD_PROXY *proxy,
+ const char *data,
+ ssize_t len)
+{
+ char *cp;
+
+ /*
+ * We still need to eliminate <CR>.
+ *
+ * <LF> is already eliminated by smtp_get_no_except() (used for BDAT), and
+ * by smtp_get() (used for DATA) which calls smtp_get_no_except().
+ */
+ if (memchr(data, '\r', len) != 0) {
+ if (proxy->data_buf == 0)
+ proxy->data_buf = vstring_alloc(100);
+ vstring_memcpy(proxy->data_buf, data, len);
+ for (cp = STR(proxy->data_buf); cp < vstring_end(proxy->data_buf); cp++)
+ if (*cp == '\r')
+ *cp = ' ';
+ data = STR(proxy->data_buf);
+ }
+ return (data);
+}
+
/* smtpd_proxy_rec_put - send message content, rec_put() clone */
static int smtpd_proxy_rec_put(VSTREAM *stream, int rec_type,
const char *data, ssize_t len)
{
const char *myname = "smtpd_proxy_rec_put";
+ SMTPD_PROXY *proxy = VSTREAM_TO_SMTPD_STATE(stream)->proxy;
int err = 0;
/*
@@ -865,16 +892,29 @@
(void) smtpd_proxy_rdwr_error(VSTREAM_TO_SMTPD_STATE(stream), err);
return (REC_TYPE_ERROR);
}
+ if (rec_type != REC_TYPE_CONT && rec_type != REC_TYPE_NORM)
+ msg_panic("%s: need REC_TYPE_NORM or REC_TYPE_CONT", myname);
+
+ /* 202608 OpenAI: fixed guard against header prepend edge cases. */
+ if (proxy->last_text_rec != REC_TYPE_CONT) {
+ /* Wietse: leading '.' may be followed by line-break crud. */
+ if (data[0] == '.' && (len == 1 || data[1] != '.')) {
+ msg_warn("prepending '.' to malformed line: '%.*s'",
+ len > 10 ? 10 : (int) len, data);
+ smtp_fwrite(".", 1, stream);
+ }
+ }
/*
* Send one content record. Errors and results must be as with rec_put().
*/
+ /* 202607 OpenAI: replace line-break crud with space. */
+ data = smtpd_proxy_fix_line_breaks(proxy, data, len);
if (rec_type == REC_TYPE_NORM)
smtp_fputs(data, len, stream);
else if (rec_type == REC_TYPE_CONT)
smtp_fwrite(data, len, stream);
- else
- msg_panic("%s: need REC_TYPE_NORM or REC_TYPE_CONT", myname);
+ proxy->last_text_rec = rec_type;
return (rec_type);
}
@@ -1014,10 +1054,11 @@
* When an operation has many arguments it is safer to use named
* parameters, and have the compiler enforce the argument count.
*/
-#define SMTPD_PROXY_ALLOC(p, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12) \
+#define SMTPD_PROXY_ALLOC(p, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, \
+ a12, a13, a14) \
((p) = (SMTPD_PROXY *) mymalloc(sizeof(*(p))), (p)->a1, (p)->a2, \
(p)->a3, (p)->a4, (p)->a5, (p)->a6, (p)->a7, (p)->a8, (p)->a9, \
- (p)->a10, (p)->a11, (p)->a12, (p))
+ (p)->a10, (p)->a11, (p)->a12, (p)->a13, (p)->a14, (p))
/*
* Sanity check.
@@ -1037,7 +1078,9 @@
rec_put = smtpd_proxy_rec_put,
flags = flags, service_stream = 0,
service_name = service, timeout = timeout,
- ehlo_name = ehlo_name, mail_from = mail_from);
+ ehlo_name = ehlo_name, mail_from = mail_from,
+ last_text_rec = 0,
+ data_buf = 0);
if (smtpd_proxy_connect(state) < 0) {
/* NOT: smtpd_proxy_free(state); we still need proxy->reply. */
return (-1);
@@ -1067,7 +1110,9 @@
rec_put = smtpd_proxy_save_rec_put,
flags = flags, service_stream = 0,
service_name = service, timeout = timeout,
- ehlo_name = ehlo_name, mail_from = mail_from);
+ ehlo_name = ehlo_name, mail_from = mail_from,
+ last_text_rec = 0,
+ data_buf = 0);
return (0);
#endif
}
@@ -1110,6 +1155,8 @@
vstring_free(proxy->request);
if (proxy->reply != 0)
vstring_free(proxy->reply);
+ if (proxy->data_buf != 0)
+ vstring_free(proxy->data_buf);
myfree((void *) proxy);
state->proxy = 0;
diff -Nru postfix-3.10.13/src/smtpd/smtpd_proxy.h postfix-3.10.14/src/smtpd/smtpd_proxy.h
--- postfix-3.10.13/src/smtpd/smtpd_proxy.h 2013-11-18 20:00:36.000000000 +0400
+++ postfix-3.10.14/src/smtpd/smtpd_proxy.h 2026-09-04 16:09:50.000000000 +0300
@@ -37,6 +37,8 @@
int timeout;
const char *ehlo_name;
const char *mail_from;
+ int last_text_rec;
+ VSTRING *data_buf;
} SMTPD_PROXY;
#define SMTPD_PROXY_FLAG_SPEED_ADJUST (1<<0)
diff -Nru postfix-3.10.13/src/tls/tls_server.c postfix-3.10.14/src/tls/tls_server.c
--- postfix-3.10.13/src/tls/tls_server.c 2024-10-11 01:15:25.000000000 +0300
+++ postfix-3.10.14/src/tls/tls_server.c 2026-09-04 00:12:43.000000000 +0300
@@ -827,6 +827,11 @@
const char *cipher_list;
TLS_APPL_STATE *app_ctx = props->ctx;
int log_mask = app_ctx->log_mask;
+ /* 202607 OpenAI: isolate session tickets by master.cf service name. */
+ EVP_MD_CTX *sessid_ctx_md = 0;
+ unsigned char sessid_ctx[EVP_MAX_MD_SIZE];
+ unsigned int sessid_ctx_len;
+ static const char sessid_ctx_label[] = "Postfix/TLS serverid";
/*
* Implicitly enable logging of trust chain errors when verified certs
@@ -868,6 +873,29 @@
TLScontext->stream = props->stream;
TLScontext->mdalg = props->mdalg;
+ /*
+ * The serverid already partitions stateful sessions in the tlsmgr
+ * session cache. Apply the same partition to stateless sessions; all
+ * services in one Postfix instance share the tlsmgr ticket keys.
+ */
+ if (tls_digest_byname(LN_sha256, &sessid_ctx_md) == 0
+ || EVP_DigestUpdate(sessid_ctx_md, sessid_ctx_label,
+ sizeof(sessid_ctx_label)) != 1
+ || EVP_DigestUpdate(sessid_ctx_md, props->serverid,
+ strlen(props->serverid) + 1) != 1
+ || EVP_DigestFinal_ex(sessid_ctx_md, sessid_ctx, &sessid_ctx_len) != 1
+ || sessid_ctx_len > SSL_MAX_SID_CTX_LENGTH
+ || SSL_set_session_id_context(TLScontext->con, sessid_ctx,
+ sessid_ctx_len) != 1) {
+ msg_warn("Could not set TLS session ID context");
+ tls_print_errors();
+ if (sessid_ctx_md)
+ EVP_MD_CTX_destroy(sessid_ctx_md);
+ tls_free_context(TLScontext);
+ return (0);
+ }
+ EVP_MD_CTX_destroy(sessid_ctx_md);
+
if (!SSL_set_ex_data(TLScontext->con, TLScontext_index, TLScontext)) {
msg_warn("Could not set application data for 'TLScontext->con'");
tls_print_errors();
diff -Nru postfix-3.10.13/src/util/mymalloc.c postfix-3.10.14/src/util/mymalloc.c
--- postfix-3.10.13/src/util/mymalloc.c 2026-06-28 23:39:06.000000000 +0300
+++ postfix-3.10.14/src/util/mymalloc.c 2026-09-04 00:12:43.000000000 +0300
@@ -197,6 +197,10 @@
#ifndef NO_SHARED_EMPTY_STRINGS
if (ptr == empty_string)
return (mymalloc(len));
+ if (len == 0) {
+ myfree(ptr);
+ return (mymalloc(0));
+ }
#endif
/*
@@ -204,7 +208,7 @@
* allows us to catch integer overflow problems that weren't already
* caught up-stream.
*/
- if (len < 1)
+ if (len < 0)
msg_panic("myrealloc: requested length %ld", (long) len);
#ifdef MYMALLOC_FUZZ
len += MYMALLOC_FUZZ;
@@ -266,7 +270,7 @@
if (len < 0)
msg_panic("mystrndup: requested length %ld", (long) len);
#ifndef NO_SHARED_EMPTY_STRINGS
- if (*str == 0)
+ if (*str == 0 || /* fix 20220615 */ len == 0)
return ((char *) empty_string);
#endif
if ((cp = memchr(str, 0, len)) != 0)
diff -Nru postfix-3.10.13/src/virtual/Makefile.in postfix-3.10.14/src/virtual/Makefile.in
--- postfix-3.10.13/src/virtual/Makefile.in 2024-03-10 00:34:49.000000000 +0300
+++ postfix-3.10.14/src/virtual/Makefile.in 2026-09-04 00:12:43.000000000 +0300
@@ -95,6 +95,7 @@
mailbox.o: ../../include/nvtable.h
mailbox.o: ../../include/recipient_list.h
mailbox.o: ../../include/safe_open.h
+mailbox.o: ../../include/sane_strtol.h
mailbox.o: ../../include/sent.h
mailbox.o: ../../include/set_eugid.h
mailbox.o: ../../include/stringops.h
diff -Nru postfix-3.10.13/src/virtual/mailbox.c postfix-3.10.14/src/virtual/mailbox.c
--- postfix-3.10.13/src/virtual/mailbox.c 2018-01-06 18:43:44.000000000 +0300
+++ postfix-3.10.14/src/virtual/mailbox.c 2026-09-04 00:12:43.000000000 +0300
@@ -57,6 +57,7 @@
#include <mymalloc.h>
#include <stringops.h>
#include <set_eugid.h>
+#include <sane_strtol.h>
/* Global library. */
@@ -176,7 +177,8 @@
const char *uid_res;
const char *gid_res;
DSN_BUF *why = state.msg_attr.why;
- long n;
+ char *end;
+ unsigned long n;
/*
* Make verbose logging easier to understand.
@@ -215,6 +217,15 @@
#define RETURN(res) { myfree(usr_attr.mailbox); return (res); }
+ if (strstr(usr_attr.mailbox + strlen(var_virt_mailbox_base), "/../")) {
+ msg_warn("recipient %s: bad mailbox path %s in %s",
+ state.msg_attr.user, mailbox_res, virtual_mailbox_maps->title);
+ dsb_simple(why, "4.3.5", "mail system configuration error");
+ *statusp = defer_append(BOUNCE_FLAGS(state.request),
+ BOUNCE_ATTR(state.msg_attr));
+ RETURN(YES);
+ }
+
/*
* Look up the mailbox owner rights. Defer in case of trouble.
*/
@@ -228,7 +239,10 @@
BOUNCE_ATTR(state.msg_attr));
RETURN(YES);
}
- if ((n = atol(uid_res)) < var_virt_minimum_uid) {
+ usr_attr.uid = (uid_t) (n = sane_strtoul(uid_res, &end, 10));
+ if (*end != 0 || errno != 0 || usr_attr.uid != n
+ || usr_attr.uid == (uid_t) - 1 /* Special for safe_open()) */
+ || usr_attr.uid < var_virt_minimum_uid) {
msg_warn("recipient %s: bad uid %s in %s",
state.msg_attr.user, uid_res, virtual_uid_maps->title);
dsb_simple(why, "4.3.5", "mail system configuration error");
@@ -236,7 +250,6 @@
BOUNCE_ATTR(state.msg_attr));
RETURN(YES);
}
- usr_attr.uid = (uid_t) n;
/*
* Look up the mailbox group rights. Defer in case of trouble.
@@ -251,7 +264,10 @@
BOUNCE_ATTR(state.msg_attr));
RETURN(YES);
}
- if ((n = atol(gid_res)) <= 0) {
+ usr_attr.gid = (gid_t) (n = sane_strtoul(gid_res, &end, 10));
+ if (*end != 0 || errno != 0 || usr_attr.gid !=n
+ || usr_attr.gid == 0 /* Backwards compatibility */
+ || usr_attr.gid == (gid_t) - 1) { /* Special for safe_open()) */
msg_warn("recipient %s: bad gid %s in %s",
state.msg_attr.user, gid_res, virtual_gid_maps->title);
dsb_simple(why, "4.3.5", "mail system configuration error");
@@ -259,12 +275,10 @@
BOUNCE_ATTR(state.msg_attr));
RETURN(YES);
}
- usr_attr.gid = (gid_t) n;
-
if (msg_verbose)
- msg_info("%s[%d]: set user_attr: %s, uid = %u, gid = %u",
+ msg_info("%s[%d]: set user_attr: %s, uid = %lu, gid = %lu",
myname, state.level, usr_attr.mailbox,
- (unsigned) usr_attr.uid, (unsigned) usr_attr.gid);
+ (unsigned long) usr_attr.uid, (unsigned long) usr_attr.gid);
/*
* Deliver to mailbox or to maildir.