fix(waf): handle empty rule bindings

Encode empty site bindings as arrays and normalize legacy JSON null values in the OpenResty runtime to prevent request-time Lua failures.
This commit is contained in:
ryan
2026-07-13 16:43:02 +08:00
parent 0c2961ae6d
commit 46941f65d5
9 changed files with 62 additions and 12 deletions
+13 -8
View File
@@ -37,6 +37,11 @@ local function warn_rate_limited(key, ...)
end
end
local function array_or_empty(value)
if type(value) == "table" then return value end
return {}
end
local function file_exists(path)
local file = io.open(path, "rb")
if not file then return false end
@@ -121,7 +126,7 @@ end
local function config_geo_requirements(config)
local uses_geo, uses_region = false, false
for _, rule in ipairs(config.rule_groups or {}) do
for _, rule in ipairs(array_or_empty(config.rule_groups)) do
for _, node in pairs((rule.graph or {}).nodes or {}) do
if node.type == "geo_match" then
uses_geo = true
@@ -270,18 +275,18 @@ local function ip_in_cidr(ip, cidr)
end
local function matches_ip_values(config, ip)
for _, item in ipairs(config.ips or {}) do
for _, item in ipairs(array_or_empty(config.ips)) do
if item == ip or ipv6_equal(item, ip) then return true end
end
for _, cidr in ipairs(config.cidrs or {}) do
for _, cidr in ipairs(array_or_empty(config.cidrs)) do
if ip_in_cidr(ip, cidr) then return true end
end
local snapshot = ip_groups_config or ip_groups_runtime.current()
local groups = (snapshot or {}).groups or {}
for _, id in ipairs(config.ip_group_ids or {}) do
for _, id in ipairs(array_or_empty(config.ip_group_ids)) do
local group = groups[tostring(id)]
if group and group.enabled then
for _, item in ipairs(group.ip_list or {}) do
for _, item in ipairs(array_or_empty(group.ip_list)) do
if item == ip or ipv6_equal(item, ip) or ip_in_cidr(ip, item) then return true end
end
end
@@ -360,13 +365,13 @@ end
local function active_rules(site)
local by_id, result = {}, {}
for _, rule in ipairs(rules_config.rule_groups or {}) do
for _, rule in ipairs(array_or_empty(rules_config.rule_groups)) do
by_id[tostring(rule.id)] = rule
if rule.enabled and rule.is_global then result[#result + 1] = rule end
end
for _, binding in ipairs(rules_config.bindings or {}) do
for _, binding in ipairs(array_or_empty(rules_config.bindings)) do
if binding.site_name == site then
for _, id in ipairs(binding.rule_group_ids or {}) do
for _, id in ipairs(array_or_empty(binding.rule_group_ids)) do
local rule = by_id[tostring(id)]
if rule and rule.enabled and not rule.is_global then result[#result + 1] = rule end
end
@@ -477,6 +477,20 @@ local function test_damaged_graphs_fail_closed()
end
end
local function test_null_binding_ids_are_treated_as_empty()
local runtime = load_runtime({
rule_groups = {},
-- cjson decodes JSON null to userdata (ngx.null). io.stdout provides the
-- same Lua value type in this standalone regression test.
bindings = { binding("null-binding", io.stdout) },
})
reset_request("null-binding")
local result = runtime.check()
assert_equal(result, "ok", "null binding IDs allow the request")
assert_equal(output.exit, nil, "null binding IDs never abort the request")
end
local function test_request_path_has_no_file_io()
local opens = 0
local original_open = io.open
@@ -530,6 +544,7 @@ test_pow_takeover_and_completion()
test_pow_internal_redirect_bypasses_graph_as_takeover()
test_block_config_and_rule_order()
test_damaged_graphs_fail_closed()
test_null_binding_ids_are_treated_as_empty()
test_request_path_has_no_file_io()
return true