Mod_security Notes
Mod_security is a tool that can help you block certain types of requests and responses on your webserver. This page is not intended to provide an overview of mod_security or even a glimpse of what it can do (see, for example, Blocking Referer Spam for that); rather, this page is intended to help those who are already familiar with the package and are trying to troubleshoot undocumented or poorly-documented behavior.
I wish a page like this existed when I was setting up mod_security — the documentation is pretty crappy and the syntax makes obfuscated C look like a work of art. It's a definitely a powerful tool and I highly recommend it, but caveat emptor. I still have no idea what the phase:1 in some of my actions does...
So here are the gotchas that got me. Note that I am using mod_security 2.5.x.
Contents |
The default rules
Mod_security comes with a set of default rules that when you first install it, will probably break parts of your site. For example, the default installation on CentOS and Fedora blocks Apache from sending a directory listing (which Apache tries to do by default in directories without an index.html), blocks some of the less-common HTTP methods (which breaks WebDAV), blocks access to certain file extensions (e.g. ini and sql), and breaks other potentially useful functionality. The configuration files on CentOS/Fedora are in /etc/httpd/modsecurity.d/. You may have to manually comment out some rules.
SecDefaultAction
This may be obvious to some, but it is not clearly defined by the documentation: SecRule and SecAction will perform the action defined in the last SecDefaultAction in addition to whatever actions (if any) you explicitly put on the SecRule or SecAction line itself. For example, consider this code:
SecDefaultAction phase:1,deny SecRule REQUEST_HEADERS:Referer \ "google\.(com|de)/group/[^./]*(poker|insur|payday)[^./]*/web/" SecRule REQUEST_HEADERS:Referer \ "(poker|insur|payday)[a-z0-9]*\.com/(item)?[0-9]+(\.html|\.php|/)$" # set environment variable for localhost, maybe to log hits separately SecRule REMOTE_ADDR "^127\.0\.0\.1$" setenv:localuser=yes
The first two SecRules will do what you want and block the referer
spammers. However, the last line will set the environment variable when
localhost visits and it will block the request. You must clear the
SecDefaultAction, for example by setting SecDefaultAction
phase:1,pass
.
Chaining rules
mod_security offers a useful action called chain. This way, you can match a request against a bunch of rules and take some action only if all of the rules match. For example, you can block a request if it is a POST and it's going to a page that should never be POSTed to.
When using chain, the documentation states that all disruptive actions must go on the first line. However, non-disruptive actions (like setting a variable or incremeting a counter) can go on any line. What they don't make clear is that only the rules up to and including that non-disruptive rule have to be true for it to be executed!
For example, consider this configuration of three rules chained together:
SecRule REQUEST_METHOD "^GET$" chain,setenv:var1=true,deny SecRule REMOTE_HOST "^127\.0\.0\.1" chain,setenv:var2=true SecRule REQUEST_URI "/denied.html" setenv:var3=true
Let's say that I now make a request for http://localhost/denied.html. All three conditions will match, and as expected, my request is denied based on the deny disruptive action on the first line, and all three vars are set to true.
However, let's say that I now make a request to http://localhost/allowed.html. This request only matches the first two conditions. From the documentation, you would expect the request to be allowed and no variables to be set. However, this is not the case. The request will be allowed, but var1 and var2 will be set to true and var3 will not be set.
httpd.conf order
Remember that httpd.conf does not execute linearly. For example, consider the following code:
SecRule REQUEST_HEADERS:Referer "viagra" flag=yes SetEnvIf flag "yes" spam=refspam
It seems that if the referer contains "viagra" then the variable flag gets set, and the next line checks whether flag is set and sets the spam variable if so. (This example is hypothetical, but similar situations have come up in my actual configuration.) However, what actually happens is that all the mod_setenvif directives are run before any of the mod_security directives, so the spam variable never gets set (because when SetEnvIf runs, flag is not set yet). Something to watch out for.
Collection expiration
The documentation is very unclear about expiration of persistant collections. The documentation for expirevar seems to imply that collections by default do not expire. However, this is not the case. The default expiration time is given later in the documentation as 3600 seconds (1 hour), but my first attempts at increasing the expiration time (useful for spam blacklists) failed mysteriously.
I'm not sure about the other collections, but the IP collection will not expire as expected if you do this:
SecAction initcol:IP=%{REMOTE_ADDR},expirevar:IP.spam=1209600
This code initializes the collection that stores data for the current request's remote IP address and sets the expiration time on the IP.spam variable to 2 weeks (1209600 seconds). But in practice, the collection expires after 3600 seconds anyways.
The reason behind this is simple, but undocumented: the IP.spam has an expiration time that is separate from the expiration time of the key it belongs to (the key is the IP address). As an example, say that the browser at 192.168.0.1 makes a request and triggers the SecAction. The collection is initialized and the expiration on IP.spam is set to 2 weeks. However, the expiration on the key itself (192.168.0.1) is still 1 hour! So after an hour, all data associated with 192.168.0.1, including the IP.spam variable, are deleted, regardless of their own expiration times.
How do we solve this problem? This is completely undocumented but works:
SecAction initcol:IP=%{REMOTE_ADDR},expirevar:IP.spam=1209600,setvar:IP.TIMEOUT=1209600
Setting the IP.TIMEOUT variable (TIMEOUT is one of the predefined variables in every collection) solves the key expiration problem. How'd I find this out? By reading hex dumps of mod_security's database files...