details: http://freenginx.org/hg/nginx/rev/cb1dc66f3f88 branches: changeset: 9492:cb1dc66f3f88 user: Maxim Dounin <[email protected]> date: Tue Mar 31 06:54:30 2026 +0300 description: 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 diffstat: src/core/ngx_string.c | 59 ++++++++++++++++++++++++++++++++++++ src/core/ngx_string.h | 1 + src/mail/ngx_mail_auth_http_module.c | 12 ++++-- src/mail/ngx_mail_proxy_module.c | 10 ++++- 4 files changed, 75 insertions(+), 7 deletions(-) diffs (155 lines): 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;
