Skip to content

Commit

Permalink
fix(router): http and stream subsystems no longer share the expre…
Browse files Browse the repository at this point in the history
…ssions router schema (#11914)

KAG-2961

---------

Co-authored-by: Datong Sun <datong.sun@konghq.com>
  • Loading branch information
2 people authored and ADD-SP committed Mar 7, 2024
1 parent 2102e94 commit fead4d2
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
message: |
Expressions route in `http` and `stream` subsystem now have stricter validation.
Previously they share the same validation schema which means admin can configure expressions
route using fields like `http.path` even for stream routes. This is no longer allowed.
type: bugfix
scope: Core
37 changes: 6 additions & 31 deletions kong/db/schema/entities/routes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,22 @@ local router = require("resty.router.router")
local deprecation = require("kong.deprecation")

local validate_route
local has_paths
do
local isempty = require("table.isempty")
local CACHED_SCHEMA = require("kong.router.atc").schema
local get_schema = require("kong.router.atc").schema
local get_expression = require("kong.router.compat").get_expression

local type = type

-- works with both `traditional_compatiable` and `expressions` routes`
validate_route = function(entity)
local schema = get_schema(entity.protocols)
local exp = entity.expression or get_expression(entity)

local ok, err = router.validate(CACHED_SCHEMA, exp)
local ok, err = router.validate(schema, exp)
if not ok then
return nil, "Router Expression failed validation: " .. err
end

return true
end

has_paths = function(entity)
local paths = entity.paths
return type(paths) == "table" and not isempty(paths)
end
end

local kong_router_flavor = kong and kong.configuration and kong.configuration.router_flavor
Expand Down Expand Up @@ -73,15 +65,8 @@ if kong_router_flavor == "expressions" then

entity_checks = {
{ custom_entity_check = {
field_sources = { "expression", "id", },
fn = function(entity)
local ok, err = validate_route(entity)
if not ok then
return nil, err
end

return true
end,
field_sources = { "expression", "id", "protocols", },
fn = validate_route,
} },
},
}
Expand Down Expand Up @@ -126,17 +111,7 @@ else
table.insert(entity_checks,
{ custom_entity_check = {
run_with_missing_fields = true,
field_sources = { "id", "paths", },
fn = function(entity)
if has_paths(entity) then
local ok, err = validate_route(entity)
if not ok then
return nil, err
end
end

return true
end,
fn = validate_route,
}}
)
end
Expand Down
53 changes: 46 additions & 7 deletions kong/router/atc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ local values_buf = buffer.new(64)


local CACHED_SCHEMA
local HTTP_SCHEMA
local STREAM_SCHEMA
do
local FIELDS = {
local HTTP_FIELDS = {

["String"] = {"net.protocol", "tls.sni",
"http.method", "http.host",
Expand All @@ -66,21 +68,39 @@ do
},

["Int"] = {"net.port",
"net.src.port", "net.dst.port",
},
}

local STREAM_FIELDS = {

["String"] = {"net.protocol", "tls.sni",
},

["Int"] = {"net.src.port", "net.dst.port",
},

["IpAddr"] = {"net.src.ip", "net.dst.ip",
},
}

CACHED_SCHEMA = schema.new()
local function generate_schema(fields)
local s = schema.new()

for typ, fields in pairs(FIELDS) do
for _, v in ipairs(fields) do
assert(CACHED_SCHEMA:add_field(v, typ))
for t, f in pairs(fields) do
for _, v in ipairs(f) do
assert(s:add_field(v, t))
end
end

return s
end

-- used by validation
HTTP_SCHEMA = generate_schema(HTTP_FIELDS)
STREAM_SCHEMA = generate_schema(STREAM_FIELDS)

-- used by running router
CACHED_SCHEMA = is_http and HTTP_SCHEMA or STREAM_SCHEMA
end


Expand Down Expand Up @@ -871,7 +891,26 @@ function _M._set_ngx(mock_ngx)
end


_M.schema = CACHED_SCHEMA
do
local protocol_to_schema = {
http = HTTP_SCHEMA,
https = HTTP_SCHEMA,
grpc = HTTP_SCHEMA,
grpcs = HTTP_SCHEMA,

tcp = STREAM_SCHEMA,
udp = STREAM_SCHEMA,
tls = STREAM_SCHEMA,

tls_passthrough = STREAM_SCHEMA,
}

-- for db schema validation
function _M.schema(protocols)
return assert(protocol_to_schema[protocols[1]])
end
end


_M.LOGICAL_OR = LOGICAL_OR
_M.LOGICAL_AND = LOGICAL_AND
Expand Down
129 changes: 128 additions & 1 deletion spec/01-unit/01-db/01-schema/06-routes_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1329,7 +1329,7 @@ describe("routes schema (flavor = traditional_compatible)", function()
reload_flavor("traditional_compatible")
setup_global_env()

it("validates a valid route", function()
it("validates a valid http route", function()
local route = {
id = a_valid_uuid,
name = "my_route",
Expand All @@ -1351,6 +1351,21 @@ describe("routes schema (flavor = traditional_compatible)", function()
assert.falsy(route.strip_path)
end)

it("validates a valid stream route", function()
local route = {
id = a_valid_uuid,
name = "my_route",
protocols = { "tcp" },
sources = { { ip = "1.2.3.4", port = 80 } },
service = { id = another_uuid },
}
route = Routes:process_auto_fields(route, "insert")
assert.truthy(route.created_at)
assert.truthy(route.updated_at)
assert.same(route.created_at, route.updated_at)
assert.truthy(Routes:validate(route))
end)

it("fails when path is invalid", function()
local route = {
id = a_valid_uuid,
Expand All @@ -1370,6 +1385,23 @@ describe("routes schema (flavor = traditional_compatible)", function()
assert.falsy(errs["@entity"])
end)

it("fails when ip address is invalid", function()
local route = {
id = a_valid_uuid,
name = "my_route",
protocols = { "tcp" },
sources = { { ip = "x.x.x.x", port = 80 } },
service = { id = another_uuid },
}
route = Routes:process_auto_fields(route, "insert")
local ok, errs = Routes:validate_insert(route)
assert.falsy(ok)
assert.truthy(errs["sources"])

-- verified by `schema/typedefs.lua`
assert.falsy(errs["@entity"])
end)

it("won't fail when rust.regex update to 1.8", function()
local route = {
id = a_valid_uuid,
Expand All @@ -1384,3 +1416,98 @@ describe("routes schema (flavor = traditional_compatible)", function()
assert.is_nil(errs)
end)
end)


describe("routes schema (flavor = expressions)", function()
local a_valid_uuid = "cbb297c0-a956-486d-ad1d-f9b42df9465a"
local another_uuid = "64a8670b-900f-44e7-a900-6ec7ef5aa4d3"

reload_flavor("expressions")
setup_global_env()

it("validates a valid http route", function()
local route = {
id = a_valid_uuid,
name = "my_route",
protocols = { "http" },
expression = [[http.method == "GET" && http.host == "example.com" && http.path == "/ovo"]],
priority = 100,
strip_path = false,
preserve_host = true,
service = { id = another_uuid },
}
route = Routes:process_auto_fields(route, "insert")
assert.truthy(route.created_at)
assert.truthy(route.updated_at)
assert.same(route.created_at, route.updated_at)
assert.truthy(Routes:validate(route))
assert.falsy(route.strip_path)
end)

it("validates a valid stream route", function()
local route = {
id = a_valid_uuid,
name = "my_route",
protocols = { "tcp" },
expression = [[net.src.ip == 1.2.3.4 && net.src.port == 80]],
priority = 100,
service = { id = another_uuid },
}
route = Routes:process_auto_fields(route, "insert")
assert.truthy(route.created_at)
assert.truthy(route.updated_at)
assert.same(route.created_at, route.updated_at)
assert.truthy(Routes:validate(route))
end)

it("fails when path is invalid", function()
local route = {
id = a_valid_uuid,
name = "my_route",
protocols = { "http" },
expression = [[http.method == "GET" && http.path ~ "/[abc/*/user$"]],
priority = 100,
service = { id = another_uuid },
}
route = Routes:process_auto_fields(route, "insert")
local ok, errs = Routes:validate_insert(route)
assert.falsy(ok)

-- verified by `schema/typedefs.lua`
assert.truthy(errs["@entity"])
end)

it("fails when ip address is invalid", function()
local route = {
id = a_valid_uuid,
name = "my_route",
protocols = { "tcp" },
expression = [[net.src.ip in 1.2.3.4/16 && net.src.port == 80]],
priority = 100,
service = { id = another_uuid },
}
route = Routes:process_auto_fields(route, "insert")
local ok, errs = Routes:validate_insert(route)
assert.falsy(ok)

-- verified by `schema/typedefs.lua`
assert.truthy(errs["@entity"])
end)

it("fails if http route's field appears in stream route", function()
local route = {
id = a_valid_uuid,
name = "my_route",
protocols = { "tcp" },
expression = [[http.method == "GET" && net.src.ip == 1.2.3.4 && net.src.port == 80]],
priority = 100,
service = { id = another_uuid },
}
route = Routes:process_auto_fields(route, "insert")
local ok, errs = Routes:validate_insert(route)
assert.falsy(ok)

-- verified by `schema/typedefs.lua`
assert.truthy(errs["@entity"])
end)
end)

0 comments on commit fead4d2

Please sign in to comment.