[PATCH] Mail: escaping of client host name

Maxim Dounin mdounin at mdounin.ru
Mon Mar 30 02:16:01 UTC 2026


# HG changeset patch
# User Maxim Dounin <mdounin at mdounin.ru>
# Date 1774836085 -10800
#      Mon Mar 30 05:01:25 2026 +0300
# Node ID bbadfe1e7cb0a100ea18e1f6f227579abd6cd637
# Parent  04172e7f25c62ee20690f369801a1a846ee04ad2
Mail: escaping of client host name.

When resolver is configured along with SMTP proxying, client host name is
determined with DNS PTR lookup (and additionally validated with A/AAAA
lookup).  Though resolved name might contain arbitrary characters, and
using it without additional validation or escaping might lead to issues
(CVE-2026-28753).

With this change, client host name is escaped when sending it to auth_http
server, as well as when sending it to the backend server in the XCLIENT
command.  In requests to auth_http it is escaped using the same URI escaping
as used for "Auth-User" and "Auth-Pass".  And in XCLIENT it is escaped
using the xtext encoding per RFC 1891 / RFC 3461, as supported by Postfix
since version 2.3.

Additionally, XCLIENT LOGIN= now also uses xtext encoding, which might be
beneficial for systems with complex logins.

See also:
https://github.com/nginx/nginx/commit/6f3145006b41a4ec464eed4093553a335d35e8ac

diff --git a/src/core/ngx_string.c b/src/core/ngx_string.c
--- a/src/core/ngx_string.c
+++ b/src/core/ngx_string.c
@@ -1964,6 +1964,65 @@ ngx_escape_json(u_char *dst, u_char *src
 }
 
 
+uintptr_t
+ngx_escape_xtext(u_char *dst, u_char *src, size_t size)
+{
+    u_char         ch;
+    ngx_uint_t     n;
+    static u_char  hex[] = "0123456789ABCDEF";
+
+    /*
+     * RFC 1891 / 3461 xtext encoding:
+     *
+     * xtext = *( xchar / hexchar )
+     *
+     * xchar = any ASCII CHAR between "!" (33) and "~" (126) inclusive,
+     *         except for "+" and "=".
+     *
+     * hexchar = ASCII "+" immediately followed by two upper case
+     *           hexadecimal digits
+     *
+     * Mostly equivalent to URI escaping, but uses "+" instead of "%".
+     */
+
+    if (dst == NULL) {
+
+        /* find the number of the characters to be escaped */
+
+        n = 0;
+
+        while (size) {
+            ch = *src++;
+
+            if (ch <= 0x20 || ch >= 0x7f || ch == '+' || ch == '=') {
+                n++;
+            }
+
+            size--;
+        }
+
+        return (uintptr_t) n;
+    }
+
+    while (size) {
+        ch = *src++;
+
+        if (ch <= 0x20 || ch >= 0x7f || ch == '+' || ch == '=') {
+            *dst++ = '+';
+            *dst++ = hex[ch >> 4];
+            *dst++ = hex[ch & 0xf];
+
+        } else {
+            *dst++ = ch;
+        }
+
+        size--;
+    }
+
+    return (uintptr_t) dst;
+}
+
+
 void
 ngx_str_rbtree_insert_value(ngx_rbtree_node_t *temp,
     ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)
diff --git a/src/core/ngx_string.h b/src/core/ngx_string.h
--- a/src/core/ngx_string.h
+++ b/src/core/ngx_string.h
@@ -212,6 +212,7 @@ uintptr_t ngx_escape_uri(u_char *dst, u_
 void ngx_unescape_uri(u_char **dst, u_char **src, size_t size, ngx_uint_t type);
 uintptr_t ngx_escape_html(u_char *dst, u_char *src, size_t size);
 uintptr_t ngx_escape_json(u_char *dst, u_char *src, size_t size);
+uintptr_t ngx_escape_xtext(u_char *dst, u_char *src, size_t size);
 
 
 typedef struct {
diff --git a/src/mail/ngx_mail_auth_http_module.c b/src/mail/ngx_mail_auth_http_module.c
--- a/src/mail/ngx_mail_auth_http_module.c
+++ b/src/mail/ngx_mail_auth_http_module.c
@@ -1267,7 +1267,7 @@ ngx_mail_auth_http_create_request(ngx_ma
 {
     size_t                     len;
     ngx_buf_t                 *b;
-    ngx_str_t                  login, passwd;
+    ngx_str_t                  login, passwd, host;
     ngx_connection_t          *c;
 #if (NGX_MAIL_SSL)
     ngx_str_t                  protocol, cipher, verify, subject, issuer,
@@ -1285,6 +1285,10 @@ ngx_mail_auth_http_create_request(ngx_ma
         return NULL;
     }
 
+    if (ngx_mail_auth_http_escape(pool, &s->host, &host) != NGX_OK) {
+        return NULL;
+    }
+
     c = s->connection;
 
 #if (NGX_MAIL_SSL)
@@ -1382,7 +1386,7 @@ ngx_mail_auth_http_create_request(ngx_ma
                 + sizeof(CRLF) - 1
           + sizeof("Client-IP: ") - 1 + s->connection->addr_text.len
                 + sizeof(CRLF) - 1
-          + sizeof("Client-Host: ") - 1 + s->host.len + sizeof(CRLF) - 1
+          + sizeof("Client-Host: ") - 1 + host.len + sizeof(CRLF) - 1
           + ahcf->header.len
           + sizeof(CRLF) - 1;
 
@@ -1483,10 +1487,10 @@ ngx_mail_auth_http_create_request(ngx_ma
                        s->connection->addr_text.len);
     *b->last++ = CR; *b->last++ = LF;
 
-    if (s->host.len) {
+    if (host.len) {
         b->last = ngx_cpymem(b->last, "Client-Host: ",
                              sizeof("Client-Host: ") - 1);
-        b->last = ngx_copy(b->last, s->host.data, s->host.len);
+        b->last = ngx_copy(b->last, host.data, host.len);
         *b->last++ = CR; *b->last++ = LF;
     }
 
diff --git a/src/mail/ngx_mail_proxy_module.c b/src/mail/ngx_mail_proxy_module.c
--- a/src/mail/ngx_mail_proxy_module.c
+++ b/src/mail/ngx_mail_proxy_module.c
@@ -646,7 +646,11 @@ ngx_mail_proxy_smtp_handler(ngx_event_t 
 
         line.len = sizeof("XCLIENT ADDR= LOGIN= NAME="
                           CRLF) - 1
-                   + s->connection->addr_text.len + s->login.len + s->host.len;
+                   + s->connection->addr_text.len
+                   + s->login.len
+                   + 2 * ngx_escape_xtext(NULL, s->login.data, s->login.len)
+                   + s->host.len
+                   + 2 * ngx_escape_xtext(NULL, s->host.data, s->host.len);
 
 #if (NGX_HAVE_INET6)
         if (s->connection->sockaddr->sa_family == AF_INET6) {
@@ -675,11 +679,11 @@ ngx_mail_proxy_smtp_handler(ngx_event_t 
 
         if (s->login.len && !pcf->smtp_auth) {
             p = ngx_cpymem(p, " LOGIN=", sizeof(" LOGIN=") - 1);
-            p = ngx_copy(p, s->login.data, s->login.len);
+            p = (u_char *) ngx_escape_xtext(p, s->login.data, s->login.len);
         }
 
         p = ngx_cpymem(p, " NAME=", sizeof(" NAME=") - 1);
-        p = ngx_copy(p, s->host.data, s->host.len);
+        p = (u_char *) ngx_escape_xtext(p, s->host.data, s->host.len);
 
         *p++ = CR; *p++ = LF;
 



More information about the nginx-devel mailing list