Skip to content

ModSecurity Exclusions for WordPress

When I upgraded ModSecurity to version 2.9.3 from the version 2.6.7 that was bundled in the Bitnami WordPress stack, I was then able to install the OWASP ModSecurity Core Rule Set (CRS) version 3.1.0, which requires ModSecurity 2.8.0 or higher. Out of the box, the CRS will throw some errors with WordPress and generally be a nuisance, but it comes packaged with a configuration option that will take care of most issues.


#SecAction \
#  id:900130,\
#  phase:1,\
#  nolog,\
#  pass,\
#  t:none,\
#  setvar:tx.crs_exclusions_drupal=1,\
#  setvar:tx.crs_exclusions_wordpress=1,\
#  setvar:tx.crs_exclusions_nextcloud=1,\
#  setvar:tx.crs_exclusions_dokuwiki=1,\
#  setvar:tx.crs_exclusions_cpanel=1"

After installing the CRS on a WordPress site, simply edit the crs-setup.conf file to un-comment these lines, and delete the non-WordPress setvar lines (as appropriate).


SecAction \
  id:900130,\
  phase:1,\
  nolog,\
  pass,\
  t:none,\
  setvar:tx.crs_exclusions_wordpress=1

This is generally all that you need to do, in order for CRS to coexist with WordPress. However, (you knew that was coming, amiright?) you might start seeing a few "Execution error - PCRE limits exceeded (-8)" lines in the error log, when you create new posts. This means that Modsecurity is running out of resources, and the likely culprit is in the modsecurity.conf configuration file.


SecResponseBodyAccess On

Changing SecResponseBodyAccess to Off should eliminate the PCRE limits exceeded messages.

The CRS installation includes a couple of empty whitelist files that you can use to further tweak the configuration - REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf and RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf. If two different whitelist files sounds confusing, the CRS team explains their use pretty well.


# As a result if one wanted to disable a rule at bootup the SecRuleRemoveById
# directive or one of its siblings would have to be placed AFTER the rule is
# listed, otherwise it will not have knowledge of the rules existence (since
# these rules are read in at the same time). This means that when using
# directives that effect SecRules, these exceptions should be placed AFTER all
# the existing rules. This is why RESPONSE-999-EXCLUSION-RULES-AFTER-CRS is
# designed such that it loads LAST.

So basically, ctl:ruleRemoveById (and other ctl: actions) will go in the "before" whitelist, and SecRuleRemoveById (and other non-ctl: actions) will go in the "after" whitelist.

If you are running a load balancer, you might want to whitelist the keep-alive function. For instance, ELB-HealthChecker on AWS can trigger several (depending the the CRS paranoia-level configuration) of the REQUEST-920-PROTOCOL-ENFORCEMENT rules. There are all kinds of different ways to whitelist the keep-alive bots - you could do it by the user agent:

REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf


# Exclude the Acme Keep-Alive from REQUEST-920-PROTOCOL-ENFORCEMENT rules
SecRule REQUEST_HEADERS:User-Agent "Acme Keep-Alive 1.12" \
"phase:1,nolog,pass,id:10011,ctl:ruleRemoveById=920300-920350"

I'm sure there are many other ways to whitelist the bot, but hopefully that will give you some ideas.

Denial Of Service Rules

The REQUEST-912-DOS-PROTECTION.conf rules are disabled by default, when you install the CRS rules. These rules are enabled just like the WordPress exclusion rules, by un-commenting them in the crs-setup.conf file:


# For a detailed description, see rule file REQUEST-912-DOS-PROTECTION.conf.
#
# Uncomment this rule to use this feature:
#
#SecAction \
# "id:900700,\
#  phase:1,\
#  nolog,\
#  pass,\
#  t:none,\
#  setvar:'tx.dos_burst_time_slice=60',\
#  setvar:'tx.dos_counter_threshold=100',\
#  setvar:'tx.dos_block_timeout=600'"

You will find a description of those variables (the "setvar" values) in the REQUEST-912-DOS-PROTECTION.conf file and it may take some trial-and-error to find the right values for your site, but I ended-up with something similar to the default values, and it is working well. You might find, though, that you trigger these rules in the WordPress admin console. Fortunately, they are easy to exclude:

RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf


<LocationMatch "^/wp-admin/.*$">
     SecRuleRemoveById 912100-912171
</LocationMatch>

This effectively disables the CRS DOS rules in the WordPress admin console, without disabling CRS, altogether (e.g. replace the action portion of the rule above, with SecRuleEngine Off).

AWS Load Balancer and Denial of Service Rules

Out of the box, the CRS DoS rules are using remote_addr to collect the client's IP address, which means that if you're sitting behind a load balancer, then remote_addr is going to be one of the load balancer's IP addresses. If the DOS rules block one of those IP addresses, then all traffic to your site will be affected.

RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf


# Change REQUEST-901-INITIALIZATION.conf to use x-forwarded-for instead of remote_addr
#
SecRuleUpdateActionById 901321 "initcol:ip=%{REQUEST_HEADERS.x-forwarded-for}_%\
{tx.ua_hash},setvar:'tx.real_ip=%{REQUEST_HEADERS.x-forwarded-for}'"

IP collection for the CRS rules occurs in the REQUEST-901-INITIALIZATION.conf file, in rule #901321. Instead of modifying the file, which would revert back to default if the CRS rules were ever updated, just copy the action part of the rule into RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf, change the remote_addr variable to REQUEST_HEADERS.x-forwarded-for, and add a SecRuleUpdateActionByID. Now when the DOS rules trigger, it will block something other than your load balancer's IP - which you can verify in the error_log.


[msg "Denial of Service (DoS) attack identified from 123.45.678.90...

The DoS message in the error log uses the same tx.real_ip variable, to print the IP address.

Limit Warnings in the Error Log


ModSecurity: Warning. Pattern match "^[\\\\d.:]+$" at REQUEST_HEADERS:Host.
ModSecurity: Warning. Operator EQ matched 0 at REQUEST_HEADERS.
ModSecurity: Warning. Match of "pm AppleWebKit Android" against "REQUEST_HEADERS

The Error Log is going to help you find any additional problems in the WordPress admin environment that you will need to whitelist, so don't limit any warnings until you have everything working smoothly, at least at a paranoia level of 2. I am on a small Amazon T2 Micro server with a single CPU and 1 GB of RAM and paranoia level 2 works just fine (as you go up in Modsecurity paranoia level, Modsecurity consumes more resources). Once it running smoothly, though, you will likely want to limit the number of warning that get recorded to the error log. This is easily accomplished by modifying some of the most common sources of the warnings, which come from the REQUEST-920-PROTOCOL-ENVORCEMENT.conf rules.


SecRuleUpdateActionById 920350 "phase:2,nolog"
SecRuleUpdateActionById 920300 "phase:2,nolog"
SecRuleUpdateActionById 920320 "phase:2,nolog"
SecRuleUpdateActionById 920280 "phase:2,nolog"
SecRuleUpdateActionById 920430 "phase:1,nolog"

The SecRuleUpdateActionByID is modifying existing rules, so these lines go in the RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf whitelist file, which gets loaded last, after the rules that you are trying to modify. Your mileage may vary, but for me, nolog-ing those five rules reduced my error log by 75% over the course of a week.

I hope that his post has given you a bit of incite into the workings of Mod_Security and the CRS rules. I'm far (really far!) from being an expert, but I began at Paranoia Level 1 and lived in my logs until I understood what was going on, and could tweak the configuration to make it do what I needed. There is no way around this learning curve, and unfortunately there is very little information for the non-expert user.

--Scrib