Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected]
Control: affects -1 + src:postfix
User: [email protected]
Usertags: pu

[ 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 <[email protected]>  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.

Reply via email to