OWASP CRS block rules in production

Web Application Firewalls (WAF) are a neat strategy to protect your webservers from malicious connections. All of the WAFs in the market work similarly. You define rules of what you think is good or bad traffic and the WAF tries to detect attacks based on that. But your WAF will always just be as good as your rules.

The OWASP CRS

Instead of having to write all your own rules, the OWASP Project has a Core Rule Set (CRS) which you can download for free from their website. The OWASP CRS project consists of many rules with different goals which you can install on your server.

Anomaly Scores

One of the neat features of mod_security together with the OWASP CRS is the anomaly score. There is an explanation of what Anomaly Scores are. In short, anomaly scoring means, that requests to your webserver run through all your WAF rules and get an anomaly score based on said rules. If that score is bigger than the anomaly score level you set, the request gets blocked and the user gets an error message instead of the expected website.

Testing strategy

When installing and running mod_security (does not matter if in Apache, nginx or another host), you come to the situation where you want to test your installation and configuration. This can quickly result in tears. Especially if you start to mix your own rules with the OWASP Core rules.

One of the pitfalls is described in this article.

One strategy to test if your WAF runs and blocks in anomaly score mode is a rule according to this example:

SecRule ARGS "@rx 203d8e31-604a-4e6a-ab85-ae0e3949ff05"
"t:none,phase:1,id:8001,setvar:tx.anomaly_score=+100000,log,msg:'Anomaly Score +100000',tag:'My WAF Test Rule'"

Installing above rule, visiting your website with an URL like this:

http://my.site.com/?argument=203d8e31-604a-4e6a-ab85-ae0e3949ff05

will get you the above “msg” and “tag” in your logfile.

If your anomaly score level is lower than 100’000, you would expect that the request will be blocked as well. Which will not happen, although everything seems to be configured allright.

Why is your own rule not blocking your request?

The problem lies within the OWASP Core Rule’s blocking rule which looks like this:

# Alert and Block based on Anomaly Scores
#
SecRule TX:ANOMALY_SCORE "@gt 0" \
    "chain,phase:2,id:'981176',t:none,deny,log,msg:'Inbound Anomaly Score Exceeded (Total Score: %{TX.ANOMALY_SCORE}, SQLi=%{TX.SQL_INJECTION_SCORE}, XSS=%{TX.XSS_SCORE}): Last Matched Message: %{tx.msg}',logdata:'Last Matched Data: %{matched_var}',setvar:tx.inbound_tx_msg=%{tx.msg},setvar:tx.inbound_anomaly_score=%{tx.anomaly_score}"
        SecRule TX:ANOMALY_SCORE "@ge %{tx.inbound_anomaly_score_level}" chain
                SecRule TX:ANOMALY_SCORE_BLOCKING "@streq on" chain
                        SecRule TX:/^\d+\-/ "(.*)"

This blocking rule basically consists of 4 sub-rules, chained together. - The first rule checks if the anomaly score is greater than 0. - The second rule checks if the anomaly score is greater or equal to your inbound anomaly score level. - The third rule checks if you activated the anomaly blocking mode. All of which will trigger with above testrule (if you activated the anomaly blocking score and the blocking mode, that is). - The fourth rule does some magic.

The bug lies in the last chained sub-rule. It (cryptically) expects, that you set a value in your testrule, something like the following:

setvar:'tx.%{rule.id}-WEB_ATTACK/DIR_TRAVERSAL-%{matched_var_name}=%{matched_var}'

If you do not set this variable in your self-written rule, the block rule will never trigger. Even if the anomaly score is high enough!

Actually that is a lie. It does trigger. In some very special circumstances I would call “by luck”: If a core rule gets triggered together with your own rule, then that last block rule will evaluate true. Because the core rule set a message.

Rules can store messages which sometimes are evaluated later on. Since we do work in Anomaly Score Mode, this will never work as expected. Many rules can trigger (and set that variable) before the block rule gets evaluated. But only the last rule’s message will be analysed. Why care in the first place then.

But how can I fix this?

You have two ways to solve this problem:

The official way

You set that var in all of your own rules. Check the Core Rules for examples, the core rules obviously have that var set. But keep in mind that this is mostly a “we make everything work again” strategy than you fixing anything at all.

The right way

Replace the blocking rule and remove the last chain. I personally prefer this method to fix the explained problem. Why so? Because I see no reason why a block rule should check for unimportant variables set by some rules.

Since you rewrite the block rule, you can also elegantly personalise other aspects of the core rule set.

  • You can as example set the status code the web server will send to the client when a block gets active.
  • Make the log a bit more parser friendly (your splunk admin will love you)

Which will result in something like this:

# Alert and Block based on Anomaly Scores
#
SecRule TX:ANOMALY_SCORE "@gt 0" \
    "chain,phase:2,id:'981176',t:none,deny,log,status:403,msg:'Inbound Anomaly Score Exceeded Score=%{TX.ANOMALY_SCORE}, SQLi=%{TX.SQL_INJECTION_SCORE}, XSS=%{TX.XSS_SCORE}', setvar:tx.inbound_tx_msg=%{tx.msg},setvar:tx.inbound_anomaly_score=%{tx.anomaly_score}"
        SecRule TX:ANOMALY_SCORE "@ge %{tx.inbound_anomaly_score_level}" chain
                SecRule TX:ANOMALY_SCORE_BLOCKING "@streq on"

Articles with similar topic