From 847f04065c82dcad2fe81d28262e6d35857c1827 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 25 Feb 2026 18:16:42 +0545 Subject: [PATCH 01/15] fix: check for invalid characters in path, query and headers Signed-off-by: Abhishek Choudhary --- lib/resty/http.lua | 48 ++++++++- t/21-header-injection.t | 220 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 t/21-header-injection.t diff --git a/lib/resty/http.lua b/lib/resty/http.lua index 0fffb431..3a209138 100644 --- a/lib/resty/http.lua +++ b/lib/resty/http.lua @@ -10,6 +10,7 @@ local str_lower = string.lower local str_upper = string.upper local str_find = string.find local str_sub = string.sub +local str_byte = string.byte local tbl_concat = table.concat local tbl_insert = table.insert local ngx_encode_args = ngx.encode_args @@ -129,6 +130,25 @@ local DEFAULT_PARAMS = { local DEBUG = false +local function validate_chars(value) + if type(value) ~= "string" then + return true + end + + for i = 1, #value do + local b = str_byte(value, i, i) + -- control characters + if b < 32 or b == 127 then + -- 9 is horizontal tab (\t) which is allowed in header values + if b ~= 9 then + return false + end + end + end + return true +end + + function _M.new(_) local sock, err = ngx_socket_tcp() if not sock then @@ -311,6 +331,11 @@ local function _format_request(self, params) local version = params.version local headers = params.headers or {} + local path = params.path + if not validate_chars(path) then + return nil, "invalid characters found in path" + end + local query = params.query or "" if type(query) == "table" then query = "?" .. ngx_encode_args(query) @@ -318,12 +343,16 @@ local function _format_request(self, params) query = "?" .. query end + if not validate_chars(query) then + return nil, "invalid characters found in query" + end + -- Initialize request local req = { str_upper(params.method), " ", self.path_prefix or "", - params.path, + path, query, HTTP[version], -- Pre-allocate slots for minimum headers and carriage return. @@ -336,14 +365,25 @@ local function _format_request(self, params) -- Append headers for key, values in pairs(headers) do key = tostring(key) + if not validate_chars(key) then + return nil, "invalid characters found in header key" + end if type(values) == "table" then for _, value in pairs(values) do + value = tostring(value) + if not validate_chars(value) then + return nil, "invalid characters found in header value" + end req[c] = key .. ": " .. tostring(value) .. "\r\n" c = c + 1 end else + values = tostring(values) + if not validate_chars(values) then + return nil, "invalid characters found in header value" + end req[c] = key .. ": " .. tostring(values) .. "\r\n" c = c + 1 end @@ -736,7 +776,11 @@ function _M.send_request(self, params) params.headers = headers -- Format and send request - local req = _format_request(self, params) + local req, err = _format_request(self, params) + if not req then + return nil, err + end + if DEBUG then ngx_log(ngx_DEBUG, "\n", req) end local bytes, err = sock:send(req) diff --git a/t/21-header-injection.t b/t/21-header-injection.t new file mode 100644 index 00000000..260b0649 --- /dev/null +++ b/t/21-header-injection.t @@ -0,0 +1,220 @@ +use Test::Nginx::Socket 'no_plan'; +use Cwd qw(cwd); + +my $pwd = cwd(); + +$ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; +$ENV{TEST_COVERAGE} ||= 0; + +our $HttpConfig = qq{ + lua_package_path "$pwd/lib/?.lua;/usr/local/share/lua/5.1/?.lua;;"; + error_log logs/error.log debug; + + init_by_lua_block { + if $ENV{TEST_COVERAGE} == 1 then + jit.off() + require("luacov.runner").init() + end + } +}; + +no_long_string(); + +run_tests(); + +__DATA__ + +=== TEST 1: Rejects header injection in path +--- http_config eval: $::HttpConfig +--- config + location = /a { + content_by_lua ' + local http = require "resty.http" + local httpc = http.new() + httpc:connect{ + scheme = "http", + host = "127.0.0.1", + port = ngx.var.server_port + } + + local res, err = httpc:request{ + method = "GET", + path = "/b\\r\\nInjected: true" + } + + if not res then + ngx.say(err) + else + ngx.say("OK") + end + + httpc:close() + '; + } +--- request +GET /a +--- response_body +invalid characters found in path +--- no_error_log +[error] + + + +=== TEST 2: Rejects header injection in query +--- http_config eval: $::HttpConfig +--- config + location = /a { + content_by_lua ' + local http = require "resty.http" + local httpc = http.new() + httpc:connect{ + scheme = "http", + host = "127.0.0.1", + port = ngx.var.server_port + } + + local res, err = httpc:request{ + method = "GET", + path = "/b", + query = "key=value\\r\\nInjected: true" + } + + if not res then + ngx.say(err) + else + ngx.say("OK") + end + + httpc:close() + '; + } +--- request +GET /a +--- response_body +invalid characters found in query +--- no_error_log +[error] + + + +=== TEST 3: Rejects header injection in header key +--- http_config eval: $::HttpConfig +--- config + location = /a { + content_by_lua ' + local http = require "resty.http" + local httpc = http.new() + httpc:connect{ + scheme = "http", + host = "127.0.0.1", + port = ngx.var.server_port + } + + local res, err = httpc:request{ + method = "GET", + path = "/b", + headers = { + ["Test-Header\\r\\nInjected"] = "value" + } + } + + if not res then + ngx.say(err) + else + ngx.say("OK") + end + + httpc:close() + '; + } +--- request +GET /a +--- response_body +invalid characters found in header key +--- no_error_log +[error] + + + +=== TEST 4: Rejects header injection in header value +--- http_config eval: $::HttpConfig +--- config + location = /a { + content_by_lua ' + local http = require "resty.http" + local httpc = http.new() + httpc:connect{ + scheme = "http", + host = "127.0.0.1", + port = ngx.var.server_port + } + + local res, err = httpc:request{ + method = "GET", + path = "/b", + headers = { + ["Test-Header"] = "value\\r\\nInjected: true" + } + } + + if not res then + ngx.say(err) + else + ngx.say("OK") + end + + httpc:close() + '; + } +--- request +GET /a +--- response_body +invalid characters found in header value +--- no_error_log +[error] + + + +=== TEST 5: Allows normal requests with valid spaces and tabs in headers +--- http_config eval: $::HttpConfig +--- config + location = /a { + content_by_lua ' + local http = require "resty.http" + local httpc = http.new() + httpc:connect{ + scheme = "http", + host = "127.0.0.1", + port = ngx.var.server_port + } + + local res, err = httpc:request{ + method = "GET", + path = "/b", + headers = { + ["Test-Header"] = "value \\t something" + } + } + + if not res then + ngx.say(err) + else + ngx.status = res.status + ngx.say(res.headers["Test-Header"]) + end + + httpc:close() + '; + } + location = /b { + content_by_lua ' + ngx.header["Test-Header"] = ngx.req.get_headers()["Test-Header"] + ngx.say("OK") + '; + } +--- request +GET /a +--- response_body +value something +--- no_error_log +[error] From bfb9555591c995480b6158393c555592a2a3e7a2 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 25 Feb 2026 21:01:03 +0545 Subject: [PATCH 02/15] fix ci Signed-off-by: Abhishek Choudhary --- .github/workflows/apisix-test.yml | 4 +--- .github/workflows/test.yml | 10 ++++------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/apisix-test.yml b/.github/workflows/apisix-test.yml index 978b8fa0..f10b6ace 100644 --- a/.github/workflows/apisix-test.yml +++ b/.github/workflows/apisix-test.yml @@ -21,9 +21,7 @@ jobs: - name: Install CPAN run: | - curl -s -L http://xrl.us/cpanm > ../cpanm - chmod +x ../cpanm - sudo mv ../cpanm /bin/cpanm + sudo apt install -y cpanminus - name: Install Test::Nginx run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 21f03e3f..4d048508 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,10 +7,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: leafo/gh-actions-lua@v8 - with: - luaVersion: "luajit-openresty" - - uses: leafo/gh-actions-luarocks@v4 + - name: Install LuaRocks + run: curl -fsSL https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh | bash - run: luarocks install luacheck - run: luacheck lib @@ -34,10 +32,10 @@ jobs: ln -s /usr/bin/bsdtar /usr/bin/tar - name: Install CPAN - run: curl -s -L http://xrl.us/cpanm > /bin/cpanm && chmod +x /bin/cpanm + run: sudo apt install -y cpanminus - name: Cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: | ~/.cpan From 68fd2640c35ac1c9c70bd7ab098325d1a6f6969d Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 25 Feb 2026 21:36:08 +0545 Subject: [PATCH 03/15] lua Signed-off-by: Abhishek Choudhary --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d048508..47dd677c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,9 @@ jobs: steps: - uses: actions/checkout@v2 - name: Install LuaRocks - run: curl -fsSL https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh | bash + run: | + apt install lua5.1 + curl -fsSL https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh | bash - run: luarocks install luacheck - run: luacheck lib From c5a7e044c4e99c4b6e74921d714e2b0fcacfab4c Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 25 Feb 2026 21:38:37 +0545 Subject: [PATCH 04/15] phix Signed-off-by: Abhishek Choudhary --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47dd677c..f70cdbf7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: ln -s /usr/bin/bsdtar /usr/bin/tar - name: Install CPAN - run: sudo apt install -y cpanminus + run: apt install -y cpanminus - name: Cache uses: actions/cache@v4 From 04c3d7af8bb734c2d1d8cb0a6517b07ff53be162 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 25 Feb 2026 21:40:35 +0545 Subject: [PATCH 05/15] sudho Signed-off-by: Abhishek Choudhary --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f70cdbf7..52d4ae0e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: - uses: actions/checkout@v2 - name: Install LuaRocks run: | - apt install lua5.1 + sudo apt -y install lua5.1 curl -fsSL https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh | bash - run: luarocks install luacheck - run: luacheck lib From eb138c2b1fd7dc1ed548a8d1c066289397465d48 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 25 Feb 2026 21:43:33 +0545 Subject: [PATCH 06/15] devvvvv Signed-off-by: Abhishek Choudhary --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 52d4ae0e..2d96ea29 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: - uses: actions/checkout@v2 - name: Install LuaRocks run: | - sudo apt -y install lua5.1 + sudo apt -y install lua5.1 liblua5.1-0-dev curl -fsSL https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh | bash - run: luarocks install luacheck - run: luacheck lib From d2078f9317035a3990d2e4922de1c2eafdd1586a Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 25 Feb 2026 21:46:11 +0545 Subject: [PATCH 07/15] sudo luarocks install Signed-off-by: Abhishek Choudhary --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d96ea29..4db3f288 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: run: | sudo apt -y install lua5.1 liblua5.1-0-dev curl -fsSL https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh | bash - - run: luarocks install luacheck + - run: sudo luarocks install luacheck - run: luacheck lib run_tests: From 5bffe7bd72013fdc5f11c57e1fb28f9fa907418b Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 25 Feb 2026 21:59:09 +0545 Subject: [PATCH 08/15] openresty Signed-off-by: Abhishek Choudhary --- .github/workflows/test.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4db3f288..78e9c3af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,19 +22,20 @@ jobs: - 1.19.3.1 runs-on: ubuntu-latest - container: - image: openresty/openresty:${{ matrix.openresty_version }}-alpine-fat - # --init runs tinit as PID 1 and prevents the 'WARNING: killing the child process' spam from the test suite - options: --init steps: - name: Install deps run: | - apk add --no-cache curl perl bash wget git perl-dev libarchive-tools + wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add - + echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/openresty.list + sudo apt-get update + sudo apt-get -y install openresty + sudo apt-get -y install curl perl bash wget git perl-dev libarchive-tools + curl -fsSL https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh | bash ln -s /usr/bin/bsdtar /usr/bin/tar - name: Install CPAN - run: apt install -y cpanminus + run: sudo apt install -y cpanminus - name: Cache uses: actions/cache@v4 @@ -45,10 +46,10 @@ jobs: key: ${{ runner.os }}-${{ matrix.openresty_version }}-cache - name: Install Test::Nginx - run: cpanm -q -n Test::Nginx + run: sudo cpanm -q -n Test::Nginx - name: Install Luacov - run: luarocks install luacov + run: sudo luarocks install luacov - uses: actions/checkout@v2 From 67c44ca9d85a41040c6971b3106839592731b60c Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 25 Feb 2026 22:03:30 +0545 Subject: [PATCH 09/15] parl Signed-off-by: Abhishek Choudhary --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 78e9c3af..6fa94664 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/openresty.list sudo apt-get update sudo apt-get -y install openresty - sudo apt-get -y install curl perl bash wget git perl-dev libarchive-tools + sudo apt-get -y install curl perl bash wget git libperl-dev libarchive-tools curl -fsSL https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh | bash ln -s /usr/bin/bsdtar /usr/bin/tar From 2034a15c83f774e67f6fc9d086ab1803e6945ce0 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 25 Feb 2026 22:14:24 +0545 Subject: [PATCH 10/15] ln -s Signed-off-by: Abhishek Choudhary --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6fa94664..bb5289b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,6 @@ jobs: sudo apt-get -y install openresty sudo apt-get -y install curl perl bash wget git libperl-dev libarchive-tools curl -fsSL https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh | bash - ln -s /usr/bin/bsdtar /usr/bin/tar - name: Install CPAN run: sudo apt install -y cpanminus From b2e46e5c11d5be9d8080d3dfd19f9dd9380c627e Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 25 Feb 2026 22:23:11 +0545 Subject: [PATCH 11/15] eksport Signed-off-by: Abhishek Choudhary --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bb5289b1..39cbb0aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,9 @@ jobs: - name: Run tests env: TEST_COVERAGE: '1' - run: /usr/bin/prove -I../test-nginx/lib t/*.t + run: | + export PATH=$OPENRESTY_PREFIX/nginx/sbin:$OPENRESTY_PREFIX/luajit/bin:$PATH + /usr/bin/prove -I../test-nginx/lib t/*.t - name: Coverage run: | From 84a1cf8e2debf331d5bd33586e8bf6629134dbdc Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 25 Feb 2026 22:33:32 +0545 Subject: [PATCH 12/15] currect path Signed-off-by: Abhishek Choudhary --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 39cbb0aa..d624a1c8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,7 +56,7 @@ jobs: env: TEST_COVERAGE: '1' run: | - export PATH=$OPENRESTY_PREFIX/nginx/sbin:$OPENRESTY_PREFIX/luajit/bin:$PATH + export PATH=/usr/local/openresty/nginx/sbin:/usr/local/openresty/luajit/bin:$PATH /usr/bin/prove -I../test-nginx/lib t/*.t - name: Coverage From 94cb54872f69c370c7a44deddd2c1743abd94811 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Thu, 26 Feb 2026 12:14:17 +0545 Subject: [PATCH 13/15] use go's httpguts and rust's hyperium to path and header validation enhancement Signed-off-by: Abhishek Choudhary --- lib/resty/http.lua | 105 ++++++++++++--- t/21-header-injection.t | 274 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 364 insertions(+), 15 deletions(-) diff --git a/lib/resty/http.lua b/lib/resty/http.lua index 3a209138..4e342dc5 100644 --- a/lib/resty/http.lua +++ b/lib/resty/http.lua @@ -129,25 +129,100 @@ local DEFAULT_PARAMS = { local DEBUG = false +-- ideated from: https://github.com/hyperium/http/blob/60fbf319500def124cabb21c8fe8533cf209ce58/src/uri/path.rs#L484-L540 +local is_allowed_path = {} +for b = 0, 255 do + is_allowed_path[b] = (b == 0x21 -- ! + or (b >= 0x24 and b <= 0x3B) -- $, %, &, ', (, ), *, +, ,, -, ., /, 0-9, :, ; + or b == 0x3D -- = + or (b >= 0x40 and b <= 0x5F) -- @, A-Z, [, \, ], ^, _ + or (b >= 0x61 and b <= 0x7A) -- a-z + or b == 0x7C -- | + or b == 0x7E -- ~ + or b == 34 -- " + or b == 123 -- { + or b == 125 -- } + or b >= 127) -- UTF-8 / DEL +end + +local function validate_path(value) + if type(value) ~= "string" then return false end -local function validate_chars(value) - if type(value) ~= "string" then - return true + local sb = string.byte + for i = 1, #value do + if not is_allowed_path[sb(value, i)] then + return false + end end + return true +end + +local is_allowed_query = {} +for b = 0, 255 do + is_allowed_query[b] = is_allowed_path[b] +end +-- Query logic matches Path logic, but adds 0x3F (?) +is_allowed_query[0x3F] = true + +local function validate_query(value) + if type(value) ~= "string" then return false end + local sb = string.byte for i = 1, #value do - local b = str_byte(value, i, i) - -- control characters - if b < 32 or b == 127 then - -- 9 is horizontal tab (\t) which is allowed in header values - if b ~= 9 then - return false - end + if not is_allowed_query[sb(value, i)] then + return false + end + end + return true +end + +-- Pre-compute the Token table (based on Go's httpguts.isTokenTable) +-- https://github.com/golang/net/blob/60b3f6f8ce12def82ae597aebe9031753198f74d/http/httpguts/httplex.go#L15-L93 +local is_token_char = {} +local tokens = "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~" + +for i = 1, #tokens do + is_token_char[string.byte(tokens, i)] = true +end + +local function validate_header_name(name) + if type(name) ~= "string" or name == "" then return false end + + local sb = string.byte + for i = 1, #name do + if not is_token_char[sb(name, i)] then + return false end end return true end +-- Pre-compute the Header Value table +local is_valid_header_value_char = {} +for b = 0, 255 do + -- Logic: Not a CTL, OR is LWS (Space 32 or Tab 9) + -- isCTL: b < 32 or b == 127 + -- isLWS: b == 32 or b == 9 + local is_ctl = (b < 32 or b == 127) + local is_lws = (b == 32 or b == 9) + + if not is_ctl or is_lws then + is_valid_header_value_char[b] = true + else + is_valid_header_value_char[b] = false + end +end + +local function validate_header_value(value) + if type(value) ~= "string" then return false end + local sb = string.byte + for i = 1, #value do + if not is_valid_header_value_char[sb(value, i)] then + return false + end + end + return true +end function _M.new(_) local sock, err = ngx_socket_tcp() @@ -332,7 +407,7 @@ local function _format_request(self, params) local headers = params.headers or {} local path = params.path - if not validate_chars(path) then + if not validate_path(path) then return nil, "invalid characters found in path" end @@ -343,7 +418,7 @@ local function _format_request(self, params) query = "?" .. query end - if not validate_chars(query) then + if not validate_query(query) then return nil, "invalid characters found in query" end @@ -365,14 +440,14 @@ local function _format_request(self, params) -- Append headers for key, values in pairs(headers) do key = tostring(key) - if not validate_chars(key) then + if not validate_header_name(key) then return nil, "invalid characters found in header key" end if type(values) == "table" then for _, value in pairs(values) do value = tostring(value) - if not validate_chars(value) then + if not validate_header_value(value) then return nil, "invalid characters found in header value" end req[c] = key .. ": " .. tostring(value) .. "\r\n" @@ -381,7 +456,7 @@ local function _format_request(self, params) else values = tostring(values) - if not validate_chars(values) then + if not validate_header_value(values) then return nil, "invalid characters found in header value" end req[c] = key .. ": " .. tostring(values) .. "\r\n" diff --git a/t/21-header-injection.t b/t/21-header-injection.t index 260b0649..47f82848 100644 --- a/t/21-header-injection.t +++ b/t/21-header-injection.t @@ -218,3 +218,277 @@ GET /a value something --- no_error_log [error] + + + +=== TEST 6: Rejects spaces in path +--- http_config eval: $::HttpConfig +--- config + location = /a { + content_by_lua ' + local http = require "resty.http" + local httpc = http.new() + httpc:connect{ + scheme = "http", + host = "127.0.0.1", + port = ngx.var.server_port + } + + local res, err = httpc:request{ + method = "GET", + path = "/foo bar" + } + + if not res then + ngx.say(err) + else + ngx.say("OK") + end + + httpc:close() + '; + } +--- request +GET /a +--- response_body +invalid characters found in path +--- no_error_log +[error] + + + +=== TEST 7: Rejects spaces in query literal +--- http_config eval: $::HttpConfig +--- config + location = /a { + content_by_lua ' + local http = require "resty.http" + local httpc = http.new() + httpc:connect{ + scheme = "http", + host = "127.0.0.1", + port = ngx.var.server_port + } + + local res, err = httpc:request{ + method = "GET", + path = "/b", + query = "key=value with space" + } + + if not res then + ngx.say(err) + else + ngx.say("OK") + end + + httpc:close() + '; + } +--- request +GET /a +--- response_body +invalid characters found in query +--- no_error_log +[error] + + + +=== TEST 8: Rejects invalid characters in header key +--- http_config eval: $::HttpConfig +--- config + location = /a { + content_by_lua ' + local http = require "resty.http" + local httpc = http.new() + httpc:connect{ + scheme = "http", + host = "127.0.0.1", + port = ngx.var.server_port + } + + local res, err = httpc:request{ + method = "GET", + path = "/b", + headers = { + ["Test:Header"] = "value" + } + } + + if not res then + ngx.say(err) + else + ngx.say("OK") + end + + httpc:close() + '; + } +--- request +GET /a +--- response_body +invalid characters found in header key +--- no_error_log +[error] + + + +=== TEST 9: Allows non-English characters (UTF-8) in path +--- http_config eval: $::HttpConfig +--- config + location = /a { + content_by_lua ' + local http = require "resty.http" + local httpc = http.new() + httpc:connect{ + scheme = "http", + host = "127.0.0.1", + port = ngx.var.server_port + } + + local res, err = httpc:request{ + method = "GET", + path = "/path/你好" + } + + if not res then + ngx.say(err) + else + ngx.say("OK") + end + + httpc:close() + '; + } + location /path/ { + echo "OK"; + } +--- request +GET /a +--- response_body +OK +--- no_error_log +[error] + + + +=== TEST 10: Allows non-English characters (UTF-8) in query +--- http_config eval: $::HttpConfig +--- config + location = /a { + content_by_lua ' + local http = require "resty.http" + local httpc = http.new() + httpc:connect{ + scheme = "http", + host = "127.0.0.1", + port = ngx.var.server_port + } + + local res, err = httpc:request{ + method = "GET", + path = "/b", + query = "key=你好" + } + + if not res then + ngx.say(err) + else + ngx.say("OK") + end + + httpc:close() + '; + } + location = /b { + echo "OK"; + } +--- request +GET /a +--- response_body +OK +--- no_error_log +[error] + + + +=== TEST 11: Allows non-English characters (UTF-8) in header value +--- http_config eval: $::HttpConfig +--- config + location = /a { + content_by_lua ' + local http = require "resty.http" + local httpc = http.new() + httpc:connect{ + scheme = "http", + host = "127.0.0.1", + port = ngx.var.server_port + } + + local res, err = httpc:request{ + method = "GET", + path = "/b", + headers = { + ["X-Test"] = "你好" + } + } + + if not res then + ngx.say(err) + else + ngx.say("OK") + end + + httpc:close() + '; + } + location = /b { + echo "OK"; + } +--- request +GET /a +--- response_body +OK +--- no_error_log +[error] + + + +=== TEST 12: Allows exotic but valid characters in path and query +--- http_config eval: $::HttpConfig +--- config + location = /a { + content_by_lua ' + local http = require "resty.http" + local httpc = http.new() + httpc:connect{ + scheme = "http", + host = "127.0.0.1", + port = ngx.var.server_port + } + + local res, err = httpc:request{ + method = "GET", + path = "/_a-Z~.!$&*+,;=:@", + query = "k=_a-Z~.!$&*+,;=:@" + } + + if not res then + ngx.say(err) + else + ngx.say("OK") + end + + httpc:close() + '; + } + location /_a-Z { + echo "OK"; + } +--- request +GET /a +--- response_body +OK +--- no_error_log +[error] + From 23472748f5760487c4991ce636499961f9f1d5aa Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Thu, 26 Feb 2026 12:14:38 +0545 Subject: [PATCH 14/15] rename Signed-off-by: Abhishek Choudhary --- t/{21-header-injection.t => 21-character-validation.t} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename t/{21-header-injection.t => 21-character-validation.t} (100%) diff --git a/t/21-header-injection.t b/t/21-character-validation.t similarity index 100% rename from t/21-header-injection.t rename to t/21-character-validation.t From f7d4fd82240cb727ddc3b5e1cd10bf8d66817593 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Thu, 26 Feb 2026 12:54:39 +0545 Subject: [PATCH 15/15] lint Signed-off-by: Abhishek Choudhary --- lib/resty/http.lua | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/resty/http.lua b/lib/resty/http.lua index 4e342dc5..9983c735 100644 --- a/lib/resty/http.lua +++ b/lib/resty/http.lua @@ -129,7 +129,10 @@ local DEFAULT_PARAMS = { local DEBUG = false --- ideated from: https://github.com/hyperium/http/blob/60fbf319500def124cabb21c8fe8533cf209ce58/src/uri/path.rs#L484-L540 +-- ideated from: +-- luacheck: push max code line length 300 +-- https://github.com/hyperium/http/blob/60fbf319500def124cabb21c8fe8533cf209ce58/src/uri/path.rs#L484-L540 +-- luacheck: pop local is_allowed_path = {} for b = 0, 255 do is_allowed_path[b] = (b == 0x21 -- ! @@ -148,9 +151,8 @@ end local function validate_path(value) if type(value) ~= "string" then return false end - local sb = string.byte for i = 1, #value do - if not is_allowed_path[sb(value, i)] then + if not is_allowed_path[str_byte(value, i)] then return false end end @@ -167,9 +169,8 @@ is_allowed_query[0x3F] = true local function validate_query(value) if type(value) ~= "string" then return false end - local sb = string.byte for i = 1, #value do - if not is_allowed_query[sb(value, i)] then + if not is_allowed_query[str_byte(value, i)] then return false end end @@ -182,15 +183,14 @@ local is_token_char = {} local tokens = "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~" for i = 1, #tokens do - is_token_char[string.byte(tokens, i)] = true + is_token_char[str_byte(tokens, i)] = true end local function validate_header_name(name) if type(name) ~= "string" or name == "" then return false end - local sb = string.byte for i = 1, #name do - if not is_token_char[sb(name, i)] then + if not is_token_char[str_byte(name, i)] then return false end end @@ -215,9 +215,8 @@ end local function validate_header_value(value) if type(value) ~= "string" then return false end - local sb = string.byte for i = 1, #value do - if not is_valid_header_value_char[sb(value, i)] then + if not is_valid_header_value_char[str_byte(value, i)] then return false end end