Skip to content

Commit 0be62f2

Browse files
committed
Implement ruleRemoveByMsg and ruleRemoveByTag actions
Use a per-worker lookup table to append rule IDs to the _ignore_rules waf object table for ctl:ruleRemoveBy* actions. This lookup table is re-generated when a new key is added to the ruleset definitions table. Both RemoveByTag and RemoveByMsg actions are merged into a nondisruptive action 'rule_removed_by_meta', which consults the pregenerated exceptions lookup table. This commit addresses part of issue #257.
1 parent dfe1b61 commit 0be62f2

File tree

13 files changed

+1069
-6
lines changed

13 files changed

+1069
-6
lines changed

lib/resty/waf.lua

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,30 @@ local _global_rulesets = {
3939
local _ruleset_defs = {}
4040
local _ruleset_def_cnt = 0
4141

42+
-- lookup tables for msg and tag exceptions
43+
-- public so it can be accessed via metatable
44+
_M._meta_exception = {
45+
msgs = {},
46+
tags = {},
47+
meta_ids = {},
48+
}
49+
50+
-- this function runs when a new ruleset has been introduced to
51+
-- _ruleset_defs. it reads over all the rules, looking for msg and tag
52+
-- elements, and building a lookup table for exceptions
53+
local function _build_exception_table()
54+
-- build a rule.id -> { rule.id, ... } lookup table based on the exception
55+
-- action for the rule
56+
57+
for r, ruleset in pairs(_ruleset_defs) do
58+
for phase, rules in pairs(ruleset) do
59+
for i, rule in ipairs(rules) do
60+
util.rule_exception(_M._meta_exception, rule)
61+
end
62+
end
63+
end
64+
end
65+
4266
-- get a subset or superset of request data collection
4367
local function _parse_collection(self, collection, var)
4468
local parse = var.parse
@@ -317,7 +341,7 @@ end
317341
local function _calculate_offset(ruleset)
318342
for phase, i in pairs(phase_t.phases) do
319343
if (ruleset[phase]) then
320-
calc.calculate(ruleset[phase])
344+
calc.calculate(ruleset[phase], _M._meta_exception)
321345
else
322346
ruleset[phase] = {}
323347
end
@@ -333,6 +357,8 @@ local function _merge_rulesets(self)
333357
t[v] = true
334358
end
335359

360+
local rebuild_exception_table = false
361+
336362
if (self) then
337363
local added = self._add_ruleset
338364
local added_s = self._add_ruleset_string
@@ -356,6 +382,9 @@ local function _merge_rulesets(self)
356382
_calculate_offset(rs)
357383

358384
_ruleset_defs[k] = rs
385+
_ruleset_def_cnt = _ruleset_def_cnt + 1
386+
387+
rebuild_exception_table = true
359388
end
360389
end
361390

@@ -368,6 +397,8 @@ local function _merge_rulesets(self)
368397
end
369398
end
370399

400+
if rebuild_exception_table then _build_exception_table() end
401+
371402
t = util.table_keys(t)
372403

373404
-- rulesets will be processed in numeric order
@@ -475,6 +506,9 @@ function _M.exec(self, opts)
475506
_calculate_offset(rs)
476507

477508
_ruleset_defs[ruleset] = rs
509+
_ruleset_def_cnt = _ruleset_def_cnt + 1
510+
511+
_build_exception_table()
478512
end
479513
end
480514

@@ -627,8 +661,9 @@ function _M.init()
627661
_calculate_offset(rs)
628662

629663
_ruleset_defs[ruleset] = rs
630-
631664
_ruleset_def_cnt = _ruleset_def_cnt + 1
665+
666+
_build_exception_table()
632667
end
633668
end
634669
end

lib/resty/waf/actions.lua

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,18 @@ _M.nondisruptive_lookup = {
8181
--_LOG_"Runtime ignoring rule " .. rule
8282

8383
waf._ignore_rule[rule] = true
84-
end
84+
end,
85+
rule_remove_by_meta = function(waf, data, ctx)
86+
--_LOG_"Runtime ignoring rules by meta"
87+
88+
-- this lookup table holds
89+
local meta_rules = waf._meta_exception.meta_ids[ctx.id] or {}
90+
91+
for i, id in ipairs(meta_rules) do
92+
--_LOG_"Runtime ignoring rule " .. id
93+
waf._ignore_rule[id] = true
94+
end
95+
end,
8596
}
8697

8798
return _M

lib/resty/waf/rule_calc.lua

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ local function _write_skip_offset(rule, max, cur_offset)
8181
end
8282
end
8383

84-
function _M.calculate(ruleset)
84+
function _M.calculate(ruleset, meta_lookup)
8585
local max = #ruleset
8686
local chain = {}
8787

@@ -122,6 +122,29 @@ function _M.calculate(ruleset)
122122

123123
chain = {}
124124
end
125+
126+
-- meta table lookups for exceptions
127+
if (meta_lookup) then
128+
if (rule.msg) then
129+
local msg = rule.msg
130+
131+
if (not meta_lookup.msgs[msg]) then
132+
meta_lookup.msgs[msg] = { rule.id }
133+
else
134+
table_insert(meta_lookup.msgs[msg], rule.id)
135+
end
136+
end
137+
138+
if (rule.tag) then
139+
for _, tag in ipairs(rule.tag) do
140+
if (not meta_lookup.tags[tag]) then
141+
meta_lookup.tags[tag] = { rule.id }
142+
else
143+
table_insert(meta_lookup.tags[tag], rule.id)
144+
end
145+
end
146+
end
147+
end
125148
end
126149
end
127150

lib/resty/waf/util.lua

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ _M.version = "0.9"
55
local cjson = require "cjson"
66
local logger = require "resty.waf.log"
77

8-
8+
local re_find = ngx.re.find
99
local string_byte = string.byte
1010
local string_char = string.char
1111
local string_format = string.format
@@ -101,6 +101,19 @@ function _M.table_has_value(needle, haystack)
101101
return false
102102
end
103103

104+
-- append the contents of (array-like) table b into table a
105+
function _M.table_append(a, b)
106+
-- handle some ugliness
107+
local c = type(b) == 'table' and b or { b }
108+
109+
local a_count = #a
110+
111+
for i = 1, #c do
112+
a_count = a_count + 1
113+
a[a_count] = c[i]
114+
end
115+
end
116+
104117
-- pick out dynamic data from storage key definitions
105118
function _M.parse_dynamic_value(waf, key, collections)
106119
local lookup = function(m)
@@ -274,4 +287,38 @@ _M.sieve_collection = {
274287
end,
275288
}
276289

290+
-- build the msg/tag exception table for a given rule
291+
function _M.rule_exception(exception_table, rule)
292+
if not rule.exceptions then
293+
return
294+
end
295+
296+
local ids = {}
297+
local count = 0
298+
299+
for i, exception in ipairs(rule.exceptions) do
300+
for key, rules in pairs(exception_table.msgs) do
301+
if (re_find(key, exception, 'jo')) then
302+
for j, id in ipairs(rules) do
303+
count = count + 1
304+
ids[count] = id
305+
end
306+
end
307+
end
308+
309+
for key, rules in pairs(exception_table.tags) do
310+
if (re_find(key, exception, 'jo')) then
311+
for j, id in ipairs(rules) do
312+
count = count + 1
313+
ids[count] = id
314+
end
315+
end
316+
end
317+
end
318+
319+
if count > 0 then
320+
exception_table.meta_ids[rule.id] = ids
321+
end
322+
end
323+
277324
return _M
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
use Test::Nginx::Socket::Lua;
2+
use Cwd qw(cwd);
3+
4+
my $pwd = cwd();
5+
6+
our $HttpConfig = qq{
7+
lua_package_path "$pwd/lib/?.lua;$pwd/t/?.lua;;";
8+
lua_package_cpath "$pwd/lib/?.lua;;";
9+
};
10+
11+
repeat_each(3);
12+
plan tests => repeat_each() * 5 * blocks();
13+
14+
no_shuffle();
15+
run_tests();
16+
17+
__DATA__
18+
19+
=== TEST 1: Ignore rules at runtime based on tag
20+
--- http_config eval: $::HttpConfig
21+
--- config
22+
location /t {
23+
access_by_lua_block {
24+
local lua_resty_waf = require "resty.waf"
25+
local waf = lua_resty_waf:new()
26+
27+
waf:set_option("debug", true)
28+
waf:set_option("mode", "ACTIVE")
29+
waf:set_option("add_ruleset", "10000_ctl_meta")
30+
31+
--[[
32+
rules for these tests:
33+
34+
SecAction "id:12345,phase:1,pass,nolog,ctl:ruleRemoveByTag=mocktag"
35+
SecAction "id:12355,phase:1,pass,nolog,ctl:ruleRemoveByMsg=mockmsg"
36+
37+
SecRule ARGS bar "id:12346,phase:1,deny,tag:'mocktag'"
38+
SecRule ARGS baz "id:12347,phase:1,deny,tag:'mocktag',tag:'othertag'"
39+
SecRule ARGS bat "id:12348,phase:1,deny,tag:'othertag'"
40+
41+
SecRule ARGS foo "id:12356,phase:1,deny,msg:'mockmsg'"
42+
SecRule ARGS frob "id:12357,phase:1,deny,msg:'othermsg'"
43+
--]]
44+
45+
waf:exec()
46+
}
47+
48+
content_by_lua_block { ngx.exit(ngx.HTTP_OK) }
49+
}
50+
--- request
51+
GET /t
52+
--- error_code: 200
53+
--- error_log
54+
Runtime ignoring rules by meta
55+
Runtime ignoring rule 12346
56+
Runtime ignoring rule 12347
57+
Ignoring rule 12346
58+
Ignoring rule 12347
59+
--- no_error_log
60+
[error]
61+
Runtime ignoring rule 12348
62+
Ignoring rule 12348
63+
64+
=== TEST 2: Matched rules not in an exception group still execute
65+
--- http_config eval: $::HttpConfig
66+
--- config
67+
location /t {
68+
access_by_lua_block {
69+
local lua_resty_waf = require "resty.waf"
70+
local waf = lua_resty_waf:new()
71+
72+
waf:set_option("debug", true)
73+
waf:set_option("mode", "ACTIVE")
74+
waf:set_option("add_ruleset", "10000_ctl_meta")
75+
76+
waf:exec()
77+
}
78+
79+
content_by_lua_block { ngx.exit(ngx.HTTP_OK) }
80+
}
81+
--- request
82+
GET /t?a=bat
83+
--- error_code: 403
84+
--- error_log
85+
Match of rule 12348
86+
--- no_error_log
87+
[error]
88+
89+
=== TEST 3: Ignore a rule at runtime based on msg
90+
--- http_config eval: $::HttpConfig
91+
--- config
92+
location /t {
93+
access_by_lua_block {
94+
local lua_resty_waf = require "resty.waf"
95+
local waf = lua_resty_waf:new()
96+
97+
waf:set_option("debug", true)
98+
waf:set_option("mode", "ACTIVE")
99+
waf:set_option("add_ruleset", "10000_ctl_meta")
100+
101+
waf:exec()
102+
}
103+
104+
content_by_lua_block { ngx.exit(ngx.HTTP_OK) }
105+
}
106+
--- request
107+
GET /t
108+
--- error_code: 200
109+
--- error_log
110+
Runtime ignoring rules by meta
111+
Ignoring rule 12356
112+
--- no_error_log
113+
[error]
114+
Ignoring rule 12357
115+
116+
=== TEST 4: Matched rules not in an exception group still execute
117+
--- http_config eval: $::HttpConfig
118+
--- config
119+
location /t {
120+
access_by_lua_block {
121+
local lua_resty_waf = require "resty.waf"
122+
local waf = lua_resty_waf:new()
123+
124+
waf:set_option("debug", true)
125+
waf:set_option("mode", "ACTIVE")
126+
waf:set_option("add_ruleset", "10000_ctl_meta")
127+
128+
waf:exec()
129+
}
130+
131+
content_by_lua_block { ngx.exit(ngx.HTTP_OK) }
132+
}
133+
--- request
134+
GET /t?a=frob
135+
--- error_code: 403
136+
--- error_log
137+
Match of rule 12357
138+
--- no_error_log
139+
[error]
140+

0 commit comments

Comments
 (0)