This is an automated email from the ASF dual-hosted git repository.
nic-6443 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new 524d34c8c5 fix: encode control characters in $upstream_uri to prevent
CRLF injection (#13787)
524d34c8c5 is described below
commit 524d34c8c5cb3f04190633164c8a0ae8fa45ab65
Author: Nic <[email protected]>
AuthorDate: Fri Aug 7 15:05:47 2026 +0800
fix: encode control characters in $upstream_uri to prevent CRLF injection
(#13787)
---
apisix/core/utils.lua | 11 +++++++++++
apisix/init.lua | 5 +++--
apisix/plugins/proxy-rewrite.lua | 6 +++++-
t/plugin/proxy-rewrite.t | 35 +++++++++++++++++++++++++++++++++++
4 files changed, 54 insertions(+), 3 deletions(-)
diff --git a/apisix/core/utils.lua b/apisix/core/utils.lua
index 469af448c9..0b8198bc70 100644
--- a/apisix/core/utils.lua
+++ b/apisix/core/utils.lua
@@ -224,6 +224,17 @@ function _M.uri_safe_encode(uri)
end
+-- escape_uri_control_chars percent-encodes control characters (0x00-0x1F and
0x7F,
+-- which include CR and LF) while leaving every other byte untouched. It is
used on
+-- parts of $upstream_uri that must keep their query delimiters (?, =, &) but
must not
+-- be able to inject CR/LF into the upstream request line.
+function _M.escape_uri_control_chars(uri)
+ return (str_gsub(uri, "[%z\1-\31\127]", function(c)
+ return str_format("%%%02X", str_byte(c))
+ end))
+end
+
+
function _M.validate_header_field(field)
for i = 1, #field do
local b = str_byte(field, i, i)
diff --git a/apisix/init.lua b/apisix/init.lua
index fe1f2f6883..dcc2622481 100644
--- a/apisix/init.lua
+++ b/apisix/init.lua
@@ -813,8 +813,9 @@ function _M.http_access_phase()
api_ctx.var.uri = new_uri
-- forward the original uri so the servlet upstream
- -- can consume the param after ';'
- api_ctx.var.upstream_uri = uri
+ -- can consume the param after ';'. Encode control characters so a
+ -- CR/LF in the decoded uri cannot inject into the upstream
request line.
+ api_ctx.var.upstream_uri = core.utils.escape_uri_control_chars(uri)
end
end
diff --git a/apisix/plugins/proxy-rewrite.lua b/apisix/plugins/proxy-rewrite.lua
index f6b74e50e9..29e8f5f028 100644
--- a/apisix/plugins/proxy-rewrite.lua
+++ b/apisix/plugins/proxy-rewrite.lua
@@ -382,8 +382,12 @@ function _M.rewrite(conf, ctx)
end
if index then
+ -- The query part (after '?') keeps its ?, =, & delimiters, but
control
+ -- characters in it (e.g. a CR/LF reflected from $uri or a regex
capture)
+ -- must still be encoded so they cannot be written verbatim into
the
+ -- upstream request line.
upstream_uri = core.utils.uri_safe_encode(sub_str(upstream_uri, 1,
index - 1)) ..
- sub_str(upstream_uri, index)
+ core.utils.escape_uri_control_chars(sub_str(upstream_uri,
index))
else
-- The '?' may come from client request '%3f' when we use
ngx.var.uri directly or
-- via regex_uri
diff --git a/t/plugin/proxy-rewrite.t b/t/plugin/proxy-rewrite.t
index 7ab2abcde4..a484409d37 100644
--- a/t/plugin/proxy-rewrite.t
+++ b/t/plugin/proxy-rewrite.t
@@ -1724,3 +1724,38 @@ GET /test/echo
x-src: from-src
--- response_body_like eval
qr/x-multi: cap-echo\nx-multi: from-src/
+
+
+
+=== TEST 66: CRLF in a reflected uri is encoded, not injected into the
upstream request line
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, {
+ uri = "/reflect*",
+ plugins = {
+ ["proxy-rewrite"] = {
+ regex_uri = {"^(/reflect.*)",
"/print_request_received?orig=$1"}
+ }
+ },
+ upstream = {
+ type = "roundrobin",
+ nodes = {["127.0.0.1:1980"] = 1}
+ }
+ })
+ if code >= 300 then ngx.status = code; ngx.say(body); return end
+
+ local http = require("resty.http")
+ local httpc = http.new()
+ local res = httpc:request_uri("http://127.0.0.1:" ..
ngx.var.server_port
+ .. "/reflect%0d%0aX-Injected:pwn")
+ ngx.print(res.body)
+ }
+ }
+--- request
+GET /t
+--- response_body eval
+qr{request_uri: /print_request_received\?orig=/reflect%0[Dd]%0[Aa]X-Injected}
+--- no_error_log
+[error]