Table of Contents
- ModSecurity rule development … pitfalls !
- 1| Easy way to debug just a certain ModSecurity rule
- 2| Using setvar / exec with chained rules
- 3| Using “initcol” to set persistent collections
- 4| Don’t forget about SecDefaultAction
- 5| Using REQUEST_BODY variable
- ModSecurity Rule Execution Order
ModSecurity rule development … pitfalls !
Recently I’m more and more involved in development of web application firewall (waf) rules, for blocking diverse attack vectors and protecting web applications.
In the course of rule writing, there were some little faults, which caused me to loose tens of hours for debugging, testing and deep duck into ModSecurity’s reference manual
I will try to summarize most of the things I have struggled with below…
1| Easy way to debug just a certain ModSecurity rule
Suppose you have tones of rules on a site having tons of traffic and you need to debug just one rule. The first thing which comes to the mind is enabling debug log, which is possible by using the following directive:
That would do the job, but enabling such verbose debug level on a high used site is a suicide. You risk to generate enormous log and try very hard to find what you need.
Instead of enabling SecDebugLog for the whole server, you can trigger verbosity for just one rule by using the “ctl:” action.
Here is how it is done:
SecRule REQUEST_URI "my_bad_string" "phase:2, id:2222, ctl: debugLogLevel=9"
The above rule will set debug log level of 9 (which is the most verbose setting) and it will be applied just for this rule.
2| Using setvar / exec with chained rules
Keep in mind, that whenever you use “setvar” for setting variables inside chained rule, the action is executed immediately after matching the rule it is inside.
Here is example:
SecRule REQUEST_URI "first_criteria_to_match" "phase:2, chain, setvar:tx.first_var=1 , id:222"
SecRule REQUEST_URI "second_criteria_to_match"
In the example above, even if only the first rule is satisfied, the setvar variable would be executed.
The same applies to “exec:” actions.
3| Using “initcol” to set persistent collections
Using initcol or “collections” (in jargon) is something very handy, especially in cases, when you want to trace a request in a multiple non-chained rules.
Let say you want to block an intruder, which tries to access certain URL more then 5 times. One way to do this is by using a collection.
Here are some example rules for doing that
SecRule REQUEST_URI "bad_request" "phase:1,chain,nolog,\
SecRule ip:counter "@gt 10" "t:none,\
SecRule user:to_block "@gt 0" "phase:2,log, deny,status:403,msg:'Blocked for 15 minutes',id:102"
Here is what happens here:
- We match the REQUEST_URI for “bad_request”
- If there is “bad_request” we initialize user and ip collections, which are equal to REMOTE_ADDR (the visitors IP address)
- While initialize the collectoins, we are increasing the ip.counter at the same time. This counter counts the hits to the “bad_request”
- Whenever the ip.counter gets 10 hits, then our chained rule is executed successfully, and sets user.to_block variable, also restarts the ip.counter. There is also “expirevar” directive, which tells mod_security how much time (in seconds) the user.to_block=1 will exists.
- The final rule (id:102) checks if user:to_block is set to 1, and if yes it blocks future requests from this IP.
Whenever using a collections, you must keep in mind the following things:
- By default every collection expires after “3600” seconds. This setting could be change by setting SecCollectionTimeout to a different setting
- In order to be able to use given collection, you MUST INITIALIZE IT before you use it.Initialization, could be done by one of the following methods:
- Initialize the collection (with initcol) in a chain starter rule, and then you would be able to use it in a next chained rules.
- Initialize the collection in a lower “phase” and you will be able to use it in all rules having bigger phase. For example if you execute “initcol” in a rule of phase:1, then all your rules with phases: 2,3,4,5 would have access to this collection.
The collections themself are stored by mod_security in a files, which are named: $collectoin.dir and $collection.pag
Where $collectoin is the name of your collection.
The collection files are stored in the directory pointed by: “SecDataDir”
4| Don’t forget about SecDefaultAction
If you by mistake, forget to include action pass or deny (for example) in your rule, keep in mind that in this case mod_security will execute the action defined by SecDefaultAction. Most of the times this is “deny“, so you must be really carefull !
5| Using REQUEST_BODY variable
Whenever you decide toe use REQUEST_BODY, you must be very careful. Here are some catchy things you may want to consider (in order to skip bunch of headaches)
5.1| REQUEST_BODY is accessible only when you have HEADER: Content-Type: application/x-www-form-urlencoded
If you try to access REQUEST_BODY variable having different Content-Type header from urlencoded OR you have NO Content-Type header atll, you won’t be able !
5.2| Use ctl:forceRequestBodyVariable=On whenever you need to access REQUEST_BODY, despite the Content-Type header
By using “ctl:forceRequestBodyVariable” in a earlier phase rule, you can assure (almost assure) that you will have REQUEST_BODY variable populated. As with all of the tricky mod_security things, there are some exception to this rule.
If you happen to get text/xml content-type, then you wont be able again to access REQUEST_BODY, unless you change your requestBodyProcessor. I have dedicated whole post about my struggles to access REQUEST_BODY while text/xml Content-Type is set.
5.3| Use alternative variable STREAM_INPUT_BODY
In order to be able to use “STREAM_INPUT_BODY” you must include the following config option:
In the ModSecurity documentation is stated, that whenever you need to use REQUEST_BODY/ARGS_POST/ARGS_POST_NAMES the use of STREAM_INPUT_BODY is the fastest and best performant choice !
ModSecurity Rule Execution Order
The rules are executing in the order they are in apache config file.
ModSecurity executes rules from each phase sequentially (phase:1 , phase:2 ….)