From 511abb19e1e1b127f6d0943ccac346211a490a35 Mon Sep 17 00:00:00 2001 From: Sergey Kandaurov Date: Tue, 4 Nov 2025 16:34:32 +0400 Subject: [PATCH] Improved host header validation. Validation is rewritten to follow RFC 3986 host syntax, based on ngx_http_parse_request_line(). The following is now rejected: - the rest of gen-delims "#", "?", "@", "[", "]" - other unwise delims <">, "<", ">", "\", "^", "`', "{", "|", "}" - IP literals with a trailing dot, missing closing bracket, or pct-encoded - a port subcomponent with invalid values - characters in upper half --- src/http/ngx_http_request.c | 181 ++++++++++++++++++++++------ src/stream/ngx_stream_core_module.c | 181 ++++++++++++++++++++++------ 2 files changed, 282 insertions(+), 80 deletions(-) diff --git a/src/http/ngx_http_request.c b/src/http/ngx_http_request.c index 533af452f..557de6696 100644 --- a/src/http/ngx_http_request.c +++ b/src/http/ngx_http_request.c @@ -2184,72 +2184,173 @@ ngx_http_process_request(ngx_http_request_t *r) ngx_int_t ngx_http_validate_host(ngx_str_t *host, ngx_pool_t *pool, ngx_uint_t alloc) { - u_char *h, ch; - size_t i, dot_pos, host_len; + u_char *h, ch; + size_t i, dot_pos, host_len; + ngx_int_t port; enum { - sw_usual = 0, - sw_literal, - sw_rest + sw_host_start = 0, + sw_host, + sw_host_ip_literal, + sw_host_end, + sw_port, } state; dot_pos = host->len; host_len = host->len; + port = 0; h = host->data; - state = sw_usual; + state = sw_host_start; for (i = 0; i < host->len; i++) { ch = h[i]; - switch (ch) { + switch (state) { - case '.': - if (dot_pos == i - 1) { - return NGX_DECLINED; - } - dot_pos = i; - break; + case sw_host_start: - case ':': - if (state == sw_usual) { - host_len = i; - state = sw_rest; - } - break; - - case '[': - if (i == 0) { - state = sw_literal; - } - break; - - case ']': - if (state == sw_literal) { - host_len = i + 1; - state = sw_rest; - } - break; - - default: - - if (ngx_path_separator(ch)) { - return NGX_DECLINED; + if (ch == '[') { + state = sw_host_ip_literal; + break; } - if (ch <= 0x20 || ch == 0x7f) { - return NGX_DECLINED; - } + state = sw_host; + + /* fall through */ + + case sw_host: if (ch >= 'A' && ch <= 'Z') { alloc = 1; + break; } + if (ch >= 'a' && ch <= 'z') { + break; + } + + if (ch >= '0' && ch <= '9') { + break; + } + + switch (ch) { + case ':': + host_len = i; + state = sw_port; + break; + case '-': + break; + case '.': + if (dot_pos == i - 1) { + return NGX_DECLINED; + } + dot_pos = i; + break; + case '_': + case '~': + /* unreserved */ + break; + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': + /* sub-delims */ + break; + case '%': + /* pct-encoded */ + break; + default: + return NGX_DECLINED; + } break; + + case sw_host_ip_literal: + + if (ch >= 'A' && ch <= 'Z') { + alloc = 1; + break; + } + + if (ch >= 'a' && ch <= 'z') { + break; + } + + if (ch >= '0' && ch <= '9') { + break; + } + + switch (ch) { + case ':': + break; + case ']': + host_len = i + 1; + state = sw_host_end; + break; + case '-': + break; + case '.': + if (dot_pos == i - 1) { + return NGX_DECLINED; + } + dot_pos = i; + break; + case '_': + case '~': + /* unreserved */ + break; + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': + /* sub-delims */ + break; + default: + return NGX_DECLINED; + } + break; + + case sw_host_end: + + if (ch == ':') { + state = sw_port; + break; + } + return NGX_DECLINED; + + case sw_port: + + if (ch >= '0' && ch <= '9') { + if (port >= 6553 && (port > 6553 || (ch - '0') > 5)) { + return NGX_DECLINED; + } + + port = port * 10 + (ch - '0'); + break; + } + return NGX_DECLINED; } } + if (state == sw_host_ip_literal) { + return NGX_DECLINED; + } + if (dot_pos == host_len - 1) { host_len--; } diff --git a/src/stream/ngx_stream_core_module.c b/src/stream/ngx_stream_core_module.c index 40951c291..a09c7c634 100644 --- a/src/stream/ngx_stream_core_module.c +++ b/src/stream/ngx_stream_core_module.c @@ -474,72 +474,173 @@ ngx_stream_core_content_phase(ngx_stream_session_t *s, ngx_int_t ngx_stream_validate_host(ngx_str_t *host, ngx_pool_t *pool, ngx_uint_t alloc) { - u_char *h, ch; - size_t i, dot_pos, host_len; + u_char *h, ch; + size_t i, dot_pos, host_len; + ngx_int_t port; enum { - sw_usual = 0, - sw_literal, - sw_rest + sw_host_start = 0, + sw_host, + sw_host_ip_literal, + sw_host_end, + sw_port, } state; dot_pos = host->len; host_len = host->len; + port = 0; h = host->data; - state = sw_usual; + state = sw_host_start; for (i = 0; i < host->len; i++) { ch = h[i]; - switch (ch) { + switch (state) { - case '.': - if (dot_pos == i - 1) { - return NGX_DECLINED; - } - dot_pos = i; - break; + case sw_host_start: - case ':': - if (state == sw_usual) { - host_len = i; - state = sw_rest; - } - break; - - case '[': - if (i == 0) { - state = sw_literal; - } - break; - - case ']': - if (state == sw_literal) { - host_len = i + 1; - state = sw_rest; - } - break; - - default: - - if (ngx_path_separator(ch)) { - return NGX_DECLINED; + if (ch == '[') { + state = sw_host_ip_literal; + break; } - if (ch <= 0x20 || ch == 0x7f) { - return NGX_DECLINED; - } + state = sw_host; + + /* fall through */ + + case sw_host: if (ch >= 'A' && ch <= 'Z') { alloc = 1; + break; } + if (ch >= 'a' && ch <= 'z') { + break; + } + + if (ch >= '0' && ch <= '9') { + break; + } + + switch (ch) { + case ':': + host_len = i; + state = sw_port; + break; + case '-': + break; + case '.': + if (dot_pos == i - 1) { + return NGX_DECLINED; + } + dot_pos = i; + break; + case '_': + case '~': + /* unreserved */ + break; + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': + /* sub-delims */ + break; + case '%': + /* pct-encoded */ + break; + default: + return NGX_DECLINED; + } break; + + case sw_host_ip_literal: + + if (ch >= 'A' && ch <= 'Z') { + alloc = 1; + break; + } + + if (ch >= 'a' && ch <= 'z') { + break; + } + + if (ch >= '0' && ch <= '9') { + break; + } + + switch (ch) { + case ':': + break; + case ']': + host_len = i + 1; + state = sw_host_end; + break; + case '-': + break; + case '.': + if (dot_pos == i - 1) { + return NGX_DECLINED; + } + dot_pos = i; + break; + case '_': + case '~': + /* unreserved */ + break; + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': + /* sub-delims */ + break; + default: + return NGX_DECLINED; + } + break; + + case sw_host_end: + + if (ch == ':') { + state = sw_port; + break; + } + return NGX_DECLINED; + + case sw_port: + + if (ch >= '0' && ch <= '9') { + if (port >= 6553 && (port > 6553 || (ch - '0') > 5)) { + return NGX_DECLINED; + } + + port = port * 10 + (ch - '0'); + break; + } + return NGX_DECLINED; } } + if (state == sw_host_ip_literal) { + return NGX_DECLINED; + } + if (dot_pos == host_len - 1) { host_len--; }