Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions apisix/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,17 @@ local function build_match_uri_keep_encoded_slash(path, current_uri)
end


-- whether the decoded path has a "." or ".." segment
local function has_dot_segment(path)
if not core.string.find(path, ".") and not core.string.find(path, "%") then
return false
end
local decoded = percent_decode(path, false) .. "/"
return core.string.find(decoded, "/./") ~= nil
or core.string.find(decoded, "/../") ~= nil
end


local function common_phase(phase_name)
local api_ctx = ngx.ctx.api_ctx
if not api_ctx then
Expand Down Expand Up @@ -887,6 +898,20 @@ function _M.http_access_phase()
end
end

-- Nginx resolved dot segments in $uri, so forward the path the route was
-- matched on.
if not (local_conf.apisix and local_conf.apisix.normalize_uri_like_servlet) then
local path = api_ctx.var.request_uri
local args_pos = core.string.find(path, "?")
if args_pos then
path = str_sub(path, 1, args_pos - 1)
end
if path ~= uri and has_dot_segment(path) then
api_ctx.var.upstream_uri = core.utils.uri_safe_encode(uri)
.. api_ctx.var.is_args .. (api_ctx.var.args or "")
end
end

-- To prevent being hacked by untrusted request_uri, here we
-- record the normalized but not rewritten uri as request_uri,
-- the original request_uri can be accessed via var.real_request_uri
Expand Down
14 changes: 14 additions & 0 deletions docs/en/latest/router-radixtree.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,20 @@ when either actually rewrites the URI (a stripped trailing slash, a servlet-styl
matching without keeping `%2F`. The fallback is safe; the encoded-slash match
simply does not apply to such requests.

#### 6. Path forwarded to the upstream

Routes are matched on the normalized `$uri`: Nginx percent-decodes the path,
resolves dot segments (including encoded ones such as `..%2F` and `%2e%2e`) and
merges consecutive slashes. When the decoded request path has no `.` or `..`
segment, APISIX forwards the original request line to the upstream, consecutive
slashes included. When it has one, APISIX forwards the matched path re-encoded,
followed by the original query string. For example, a request to
`/admin/..%2Fapi/x` matches a route for `/api/*` and the upstream receives
`/api/x`.

Plugins that set the upstream URI, such as `proxy-rewrite`, still take
precedence.

### How to filter route by Nginx built-in variable?

Nginx provides a variety of built-in variables that can be used to filter routes based on certain criteria. Here is an example of how to filter routes by Nginx built-in variables:
Expand Down
6 changes: 6 additions & 0 deletions docs/zh/latest/router-radixtree.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,12 @@ apisix:

该选项会让位于 `delete_uri_tail_slash` 和 `normalize_uri_like_servlet`:等价性检查是与这两个选项处理之后的 URI 比较的,因此当其中任一确实改写了 URI(去掉末尾斜杠、剥离 servlet 风格的 `;` 参数)时,检查将不再成立,请求会回退到普通匹配而不保留 `%2F`。这种回退是安全的,只是保留编码斜杠的匹配对这类请求不再生效。

#### 6. 转发给上游的路径

路由基于归一化后的 `$uri` 进行匹配:Nginx 会对路径进行百分号解码、解析点段(包括 `..%2F`、`%2e%2e` 等编码形式)并合并连续斜杠。当解码后的请求路径不含 `.` 或 `..` 段时,APISIX 将原始请求行(包括连续斜杠)转发给上游。当路径包含这类段时,APISIX 会转发重新编码后的匹配路径,并附带原始查询字符串。例如,请求 `/admin/..%2Fapi/x` 会匹配 `/api/*` 路由,上游收到的是 `/api/x`。

设置上游 URI 的插件(如 `proxy-rewrite`)仍然优先生效。

### 如何通过 Nginx 内置变量过滤路由

具体参数及使用方式请查看 [radixtree#new](https://github.com/api7/lua-resty-radixtree#new) 文档,下面是一个简单的示例:
Expand Down
185 changes: 185 additions & 0 deletions t/router/radixtree-uri-normalized-upstream.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
use t::APISIX 'no_plan';

repeat_each(1);
log_level('info');
worker_connections(256);
no_root_location();
no_shuffle();

add_block_preprocessor(sub {
my ($block) = @_;

if (!$block->upstream_server_config) {
$block->set_value("upstream_server_config", <<'_EOC_');
location /admin/ {
content_by_lua_block {
ngx.say("request-uri=", ngx.var.request_uri)
}
}

location /api/ {
content_by_lua_block {
ngx.say("request-uri=", ngx.var.request_uri)
}
}

location = /fixed {
content_by_lua_block {
ngx.say("request-uri=", ngx.var.request_uri)
}
}
_EOC_
}

if (!$block->request) {
$block->set_value("request", "GET /t");
}
});

run_tests();

__DATA__

=== TEST 1: set routes with and without auth on the same upstream
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local routes = {
{
id = "admin",
uri = "/admin/*",
plugins = {
["key-auth"] = {},
},
},
{
id = "api",
uri = "/api/*",
},
{
id = "rewrite",
uri = "/rewrite/*",
plugins = {
["proxy-rewrite"] = {
uri = "/fixed",
},
},
},
}

for _, route in ipairs(routes) do
route.upstream = {
nodes = { ["127.0.0.1:1980"] = 1 },
type = "roundrobin",
}

local code, body = t("/apisix/admin/routes/" .. route.id,
ngx.HTTP_PUT, route)
if code >= 300 then
ngx.status = code
ngx.say(body)
return
end
end

ngx.say("passed")
}
}
--- response_body
passed



=== TEST 2: route with key-auth requires an API key
--- request
GET /admin/x
--- error_code: 401
--- response_body
{"message":"Missing API key in request"}



=== TEST 3: encoded slash dot segment forwards the matched path
--- request
GET /admin/..%2Fapi/x
--- response_body
request-uri=/api/x



=== TEST 4: encoded dot segment forwards the matched path with args
--- request
GET /admin/%2e%2e/api/x?a=1&b=%2F
--- response_body
request-uri=/api/x?a=1&b=%2F



=== TEST 5: plain dot segment forwards the matched path
--- request
GET /admin/../api/x
--- response_body
request-uri=/api/x



=== TEST 6: merged slashes alone are forwarded unchanged
--- request
GET /api/https://example.com//x
--- response_body
request-uri=/api/https://example.com//x



=== TEST 7: encoded path without normalization is forwarded unchanged
--- request
GET /api/a%2Fb%20c?x=1
--- response_body
request-uri=/api/a%2Fb%20c?x=1



=== TEST 8: proxy-rewrite uri still takes precedence
--- request
GET /rewrite/a/..%2Fb
--- response_body
request-uri=/fixed



=== TEST 9: remove the routes
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
for _, id in ipairs({"admin", "api", "rewrite"}) do
local code, body = t("/apisix/admin/routes/" .. id, ngx.HTTP_DELETE)
if code >= 300 then
ngx.status = code
ngx.say(body)
return
end
end

ngx.say("passed")
}
}
--- response_body
passed
Loading