[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