mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 23:16:37 +08:00
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:
@@ -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
|
||||
|
||||
@@ -223,6 +223,8 @@ func TestBuildCurrentConfigBundleEnablesGlobalPoWWithoutExplicitBinding(t *testi
|
||||
continue
|
||||
}
|
||||
foundWAFConfig = true
|
||||
assert.Contains(t, file.Content, `"rule_group_ids":[]`)
|
||||
assert.NotContains(t, file.Content, `"rule_group_ids":null`)
|
||||
require.NoError(t, json.Unmarshal([]byte(file.Content), &wafRuntime))
|
||||
}
|
||||
require.True(t, foundWAFConfig, "expected rendered WAF support file")
|
||||
|
||||
@@ -370,7 +370,7 @@ func buildSnapshotWAFDocument(ctx context.Context, routes []*model.ProxyRoute) (
|
||||
bindings = append(bindings, snapshotWAFBinding{
|
||||
RouteID: routeID,
|
||||
SiteName: siteName,
|
||||
RuleGroupIDs: groupIDsByRoute[routeID],
|
||||
RuleGroupIDs: nonNilUintSlice(groupIDsByRoute[routeID]),
|
||||
})
|
||||
}
|
||||
sort.Slice(bindings, func(i, j int) bool {
|
||||
@@ -382,6 +382,13 @@ func buildSnapshotWAFDocument(ctx context.Context, routes []*model.ProxyRoute) (
|
||||
return snapshotWAFDocument{RuleGroups: ruleGroups, IPGroups: ipGroups, Bindings: bindings}, nil
|
||||
}
|
||||
|
||||
func nonNilUintSlice(values []uint) []uint {
|
||||
if values == nil {
|
||||
return make([]uint, 0)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func validateSnapshotWAFIPGroupSize(groups []snapshotWAFIPGroup) error {
|
||||
runtimeGroups := make(map[string]protocol.WAFIPGroup, len(groups))
|
||||
for _, group := range groups {
|
||||
|
||||
@@ -85,6 +85,26 @@ func TestWAFGraphSnapshotPreservesOrderAndGraphReferences(t *testing.T) {
|
||||
assert.NotContains(t, string(raw), "ip_whitelist")
|
||||
}
|
||||
|
||||
func TestWAFGraphSnapshotEncodesEmptyBindingsAsArrays(t *testing.T) {
|
||||
cleanup := setupConfigVersionTestDB(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
route := &model.ProxyRoute{SiteName: "empty-binding.example.com", OriginURL: "http://origin:8080", Upstreams: `["http://origin:8080"]`, Enabled: true}
|
||||
require.NoError(t, model.CreateProxyRouteRecord(ctx, route))
|
||||
createSnapshotZoneDomains(t, ctx, route, route.SiteName)
|
||||
|
||||
snapshot, err := buildSnapshotWAFDocument(ctx, []*model.ProxyRoute{route})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, snapshot.Bindings, 1)
|
||||
require.NotNil(t, snapshot.Bindings[0].RuleGroupIDs)
|
||||
|
||||
raw, err := json.Marshal(snapshot)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(raw), `"rule_group_ids":[]`)
|
||||
assert.NotContains(t, string(raw), `"rule_group_ids":null`)
|
||||
}
|
||||
|
||||
func TestBuildSnapshotRejectsInvalidWAFGraph(t *testing.T) {
|
||||
cleanup := setupConfigVersionTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
Reference in New Issue
Block a user