From 5e297f39ee7c61eea0c8daa7bfd55adfc51148d9 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Mon, 10 Mar 2025 18:59:12 -0400 Subject: [PATCH] Proxy: escape ';' in URI paths This fixes a security hole when used with many Java application servers, which treat ';' as a special character in paths. See [1] for more details. This changes how NGINX constructs requests for its backends. Something like: location /fake1/ { proxy_pass http://127.0.0.1:8000/fake2/; } will now escape semicolons in the provided URL, provided that the original URL had any %-escapes. To prevent such bypasses, configurations can use: if ($request_uri !~ "^/[^;?#]*(?:\?|$)") { return 400 "Path parameters not allowed"; } This is ultimately a flaw in NGINX, not the backend servers: it is a client error to make an HTTP request with a URI that contains a path with a character that is not valid in paths, and as the client NGINX is responsible for only making valid requests. This is not a complete fix. A complete fix would require blocking %2F in paths (HAProxy and NGINX disagree on its meaning) and blocking unescaped semicolons in paths (NGINX and Java app servers disagree on their meaning). This patch allows closing the remaining bypasses by doing the following in configuration files: 1. Do not use regular expressions in paths unless careful. 2. Ensure all location blocks are either "location = /something" (note the = operator) or location /something/ (trailing slash). 3. Include the following at server level: if ($request_uri !~ "^(?/[^;?#]*)(?:\?|$)") { return 400 "Path parameters not allowed"; } if ($non_normalized_path ~ "%2[EeFf]|//|/\.\.?(?:\?|/|$)") { return 400 "Path has escaped . or /, repeated /, or . or .. component"; } This checks that path components are not empty and are not "." or "..". It also checks that there are no path parameters and no escaped "." or "/" characters. Paths that meet these requirements will be interpreted consistently by all web servers I am aware of. One can consider this snippet to be licensed under the MIT-0 license (no attribution required, use for any purpose allowed with no restrictions). Without this patch to NGINX, normalized URLs submitted to backends would have unescaped semicolons even if the URL submitted to NGINX escaped the semicolon. Preventing the bypass would therefore require blocking *escaped* semicolons, which should not be required. [1]: https://i.blackhat.com/us-18/Wed-August-8/us-18-Orange-Tsai-Breaking-Parser-Logic-Take-Your-Path-Normalization-Off-And-Pop-0days-Out-2.pdf --- src/core/ngx_string.c | 25 ++++++++++++++++++--- src/core/ngx_string.h | 1 + src/http/modules/ngx_http_proxy_module.c | 8 +++---- src/http/modules/ngx_http_proxy_v2_module.c | 4 ++-- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/core/ngx_string.c b/src/core/ngx_string.c index 10fe764c3..33ea63de6 100644 --- a/src/core/ngx_string.c +++ b/src/core/ngx_string.c @@ -1654,13 +1654,32 @@ ngx_escape_uri(u_char *dst, u_char *src, size_t size, ngx_uint_t type) 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ }; + /* " ", "#", "%", ";", "?", not allowed */ + + static uint32_t uri_path[] = { + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + + /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */ + 0xd800002d, /* 1101 1000 0000 0000 0000 0000 0010 1101 */ + + /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */ + 0x50000000, /* 0101 0000 0000 0000 0000 0000 0000 0000 */ + + /* ~}| {zyx wvut srqp onml kjih gfed cba` */ + 0xb8000001, /* 1011 1000 0000 0000 0000 0000 0000 0001 */ + + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + }; + static uint32_t *map[] = { uri, args, uri_component, html, refresh, memcached, memcached, - mail_xtext }; + mail_xtext, uri_path }; static u_char map_char[] = - { '%', '%', '%', '%', '%', '%', '%', '+' }; - + { '%', '%', '%', '%', '%', '%', '%', '+', '%' }; escape = map[type]; prefix = map_char[type]; diff --git a/src/core/ngx_string.h b/src/core/ngx_string.h index 183a20521..0c643ed69 100644 --- a/src/core/ngx_string.h +++ b/src/core/ngx_string.h @@ -204,6 +204,7 @@ u_char *ngx_utf8_cpystrn(u_char *dst, u_char *src, size_t n, size_t len); #define NGX_ESCAPE_MEMCACHED 5 #define NGX_ESCAPE_MAIL_AUTH 6 #define NGX_ESCAPE_MAIL_XTEXT 7 +#define NGX_ESCAPE_URI_PATH 8 #define NGX_UNESCAPE_URI 1 #define NGX_UNESCAPE_REDIRECT 2 diff --git a/src/http/modules/ngx_http_proxy_module.c b/src/http/modules/ngx_http_proxy_module.c index 7e08df702..64df665d7 100644 --- a/src/http/modules/ngx_http_proxy_module.c +++ b/src/http/modules/ngx_http_proxy_module.c @@ -1130,7 +1130,7 @@ ngx_http_proxy_create_key(ngx_http_request_t *r) if (r->quoted_uri || r->internal) { escape = 2 * ngx_escape_uri(NULL, r->uri.data + loc_len, - r->uri.len - loc_len, NGX_ESCAPE_URI); + r->uri.len - loc_len, NGX_ESCAPE_URI_PATH); } else { escape = 0; } @@ -1151,7 +1151,7 @@ ngx_http_proxy_create_key(ngx_http_request_t *r) if (escape) { ngx_escape_uri(p, r->uri.data + loc_len, - r->uri.len - loc_len, NGX_ESCAPE_URI); + r->uri.len - loc_len, NGX_ESCAPE_URI_PATH); p += r->uri.len - loc_len + escape; } else { @@ -1243,7 +1243,7 @@ ngx_http_proxy_create_request(ngx_http_request_t *r) if (r->quoted_uri || r->internal) { escape = 2 * ngx_escape_uri(NULL, r->uri.data + loc_len, - r->uri.len - loc_len, NGX_ESCAPE_URI); + r->uri.len - loc_len, NGX_ESCAPE_URI_PATH); } uri_len = ctx->vars.uri.len + r->uri.len - loc_len + escape @@ -1368,7 +1368,7 @@ ngx_http_proxy_create_request(ngx_http_request_t *r) if (escape) { ngx_escape_uri(b->last, r->uri.data + loc_len, - r->uri.len - loc_len, NGX_ESCAPE_URI); + r->uri.len - loc_len, NGX_ESCAPE_URI_PATH); b->last += r->uri.len - loc_len + escape; } else { diff --git a/src/http/modules/ngx_http_proxy_v2_module.c b/src/http/modules/ngx_http_proxy_v2_module.c index 0be5691aa..39341ece9 100644 --- a/src/http/modules/ngx_http_proxy_v2_module.c +++ b/src/http/modules/ngx_http_proxy_v2_module.c @@ -406,7 +406,7 @@ ngx_http_proxy_v2_create_request(ngx_http_request_t *r) if (r->quoted_uri || r->internal) { escape = 2 * ngx_escape_uri(NULL, r->uri.data + loc_len, - r->uri.len - loc_len, NGX_ESCAPE_URI); + r->uri.len - loc_len, NGX_ESCAPE_URI_PATH); } uri_len = ctx->ctx.vars.uri.len + r->uri.len - loc_len + escape @@ -647,7 +647,7 @@ ngx_http_proxy_v2_create_request(ngx_http_request_t *r) if (escape) { ngx_escape_uri(p, r->uri.data + loc_len, - r->uri.len - loc_len, NGX_ESCAPE_URI); + r->uri.len - loc_len, NGX_ESCAPE_URI_PATH); p += r->uri.len - loc_len + escape; } else {