|
| 1 | +From 2bf4601a94c2a22b715d74e6d92dedb1850d56d3 Mon Sep 17 00:00:00 2001 |
| 2 | +From: Dmitry Volyntsev <xeioex@nginx.com> |
| 3 | +Date: Tue, 12 May 2026 16:52:56 -0700 |
| 4 | +Subject: [PATCH] Fetch: fixed heap buffer overflow in proxy URL credentials. |
| 5 | + |
| 6 | +The destination buffers for the decoded user and password in |
| 7 | +ngx_js_parse_proxy_url() were a fixed 128 bytes, while the encoded |
| 8 | +input length was bounded only by the URL length. Since |
| 9 | +ngx_unescape_uri() writes at most one byte per input byte, raw |
| 10 | +credentials longer than 128 bytes overflowed the buffer; the |
| 11 | +length check ran only after the decode. |
| 12 | + |
| 13 | +The fix is to size the destination buffer based on the encoded |
| 14 | +input length. |
| 15 | + |
| 16 | +The bug appeared in dea8318 (0.9.4). |
| 17 | + |
| 18 | +Upstream patch reference: https://github.com/nginx/njs/commit/2bf4601a94c2a22b715d74e6d92dedb1850d56d3.patch |
| 19 | +--- |
| 20 | + nginx/ngx_js.c | 18 ++++++++---------- |
| 21 | + nginx/t/js_fetch_proxy_variable.t | 11 ++++++++++- |
| 22 | + 2 files changed, 18 insertions(+), 11 deletions(-) |
| 23 | + |
| 24 | +diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c |
| 25 | +index da50d766a..f3ca74d85 100644 |
| 26 | +--- a/nginx/ngx_js.c |
| 27 | ++++ b/nginx/ngx_js.c |
| 28 | +@@ -3549,12 +3549,12 @@ ngx_js_parse_proxy_url(ngx_pool_t *pool, ngx_log_t *log, ngx_str_t *url, |
| 29 | + pass_start = colon + 1; |
| 30 | + pass_len = at - pass_start; |
| 31 | + |
| 32 | +- decoded_user = ngx_pnalloc(pool, 128); |
| 33 | ++ decoded_user = ngx_pnalloc(pool, user_len); |
| 34 | + if (decoded_user == NULL) { |
| 35 | + return NGX_ERROR; |
| 36 | + } |
| 37 | + |
| 38 | +- decoded_pass = ngx_pnalloc(pool, 128); |
| 39 | ++ decoded_pass = ngx_pnalloc(pool, pass_len); |
| 40 | + if (decoded_pass == NULL) { |
| 41 | + return NGX_ERROR; |
| 42 | + } |
| 43 | +@@ -3562,24 +3562,22 @@ ngx_js_parse_proxy_url(ngx_pool_t *pool, ngx_log_t *log, ngx_str_t *url, |
| 44 | + p = user_start; |
| 45 | + decoded_end = decoded_user; |
| 46 | + ngx_unescape_uri(&decoded_end, &p, user_len, NGX_UNESCAPE_URI); |
| 47 | +- |
| 48 | + user_len = decoded_end - decoded_user; |
| 49 | +- if (user_len == 0 || user_len > 127) { |
| 50 | ++ |
| 51 | ++ if (user_len == 0) { |
| 52 | + ngx_log_error(NGX_LOG_ERR, log, 0, |
| 53 | +- "js_fetch_proxy username invalid or too long " |
| 54 | +- "(max 127 bytes after decoding)"); |
| 55 | ++ "js_fetch_proxy username is empty"); |
| 56 | + return NGX_ERROR; |
| 57 | + } |
| 58 | + |
| 59 | + p = pass_start; |
| 60 | + decoded_end = decoded_pass; |
| 61 | + ngx_unescape_uri(&decoded_end, &p, pass_len, NGX_UNESCAPE_URI); |
| 62 | +- |
| 63 | + pass_len = decoded_end - decoded_pass; |
| 64 | +- if (pass_len == 0 || pass_len > 127) { |
| 65 | ++ |
| 66 | ++ if (pass_len == 0) { |
| 67 | + ngx_log_error(NGX_LOG_ERR, log, 0, |
| 68 | +- "js_fetch_proxy password invalid or too long " |
| 69 | +- "(max 127 bytes after decoding)"); |
| 70 | ++ "js_fetch_proxy password is empty"); |
| 71 | + return NGX_ERROR; |
| 72 | + } |
| 73 | + |
| 74 | +diff --git a/nginx/t/js_fetch_proxy_variable.t b/nginx/t/js_fetch_proxy_variable.t |
| 75 | +index 7399d28f6..b1fcbadda 100644 |
| 76 | +--- a/nginx/t/js_fetch_proxy_variable.t |
| 77 | ++++ b/nginx/t/js_fetch_proxy_variable.t |
| 78 | +@@ -59,6 +59,12 @@ http { |
| 79 | + js_fetch_proxy $proxy_url; |
| 80 | + js_content test.http_fetch; |
| 81 | + } |
| 82 | ++ |
| 83 | ++ location /dynamic_user_proxy { |
| 84 | ++ set $proxy_url "http://$arg_user:p@127.0.0.1:%%PORT_8081%%"; |
| 85 | ++ js_fetch_proxy $proxy_url; |
| 86 | ++ js_content test.http_fetch; |
| 87 | ++ } |
| 88 | + } |
| 89 | + |
| 90 | + server { |
| 91 | +@@ -128,7 +134,7 @@ $t->write_file('test.js', <<EOF); |
| 92 | + |
| 93 | + EOF |
| 94 | + |
| 95 | +-$t->try_run('no js_fetch_proxy')->plan(3); |
| 96 | ++$t->try_run('no js_fetch_proxy')->plan(4); |
| 97 | + |
| 98 | + ############################################################################### |
| 99 | + |
| 100 | +@@ -138,5 +144,8 @@ like(http_get('/dynamic_proxy'), qr/PROXY:Basic\s+dGVzdHVzZXI6dGVzdHBhc3M=/, |
| 101 | + 'dynamic proxy URL with auth'); |
| 102 | + like(http_get('/dynamic_empty_proxy'), qr/ORIGIN:OK/, |
| 103 | + 'dynamic empty proxy URL bypasses proxy'); |
| 104 | ++like(http_get('/dynamic_user_proxy?user=' . ('a' x 200)), |
| 105 | ++ qr/PROXY:BAD-AUTH/, |
| 106 | ++ 'long user in dynamic proxy URL decoded without overflow'); |
| 107 | + |
| 108 | + ############################################################################### |
0 commit comments