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]

Reply via email to