OWASP ModSecurity Core Rules Paranoia Mode: Mechanics Proposal


The preparations for the OWASP ModSecurity Core Rules Paranoia Mode pull request are continuing. We identified around 45 rules to move into such a mode and I launched a core rules mailinglist discussion on eight or nine controversial cases. See the OWASP wiki for more infos.

Given the progress we are making on that front, it’s time to discuss the exact mechanism. How would paranoia mode work and how would you configure it? The base question is, if this is an on / off setting or something more advanced; an integer paranoia level perhaps?

This initial release is just a start. We do not know where this is leading. On/off is too limiting in my eyes. I admit, assigning invidivual rules to different levels is very difficult right now. This might change with more experience and more data. Still, we are planning to add stricter clones to existing rules. And if we talk about multiple stricter copies of the same rule, then creating multiple paranoid levels seems a must.

If we start with multiple levels right now, then rearranging rules over the levels in future versions is fairly simple. But advancing from an on/off setting would be painful for the users.

But what levels would be useful? Paranoid Level 0-5? Or 2-5? After all, 2-5 is used for the scoring of the core rules. I think we should use paranoia levels 0 – 40 for now with 10, 20, 30 and 40 being actively used by the core rules and the remaining numbers prepared for local or a possible future use. 0-40 is arbitrary, but I think it makes some sense. If we work in an integer level higher than the anomaly scores, misunderstandings are less likely. If we leave some room between the numbers, we have room to fill them in the future.

Using ModSecurity and the Core Rules comes with a certain level of paranoia by the user. That’s why I think, the default should not be 0. Existing Core Rules should not be assigned Paranoia Level 0. However, I can think of rules, that are so basic and so flawless when cutting between legitimate positives and false positives, that they could run in a paranoia level 0.

But for most of the existing Core Rules, I think a paranoia level of 10 would be suitable right now with the option of shifting them around in the future. Our knowledge is only very coarse. We need more data and using paranoid mode will hopefully bring that data.

I would then assign the following definitions to the paranoia levels:

0 – flawless rules of high quality with virtually no false positives
10 – standard rules with low false positive rate and/or very high security value.
20 – Rules with an elevated false positive rate and or adequate security value
30 – Rules with a high false positive rate and or adequate security value
40 – Rules with a very high false positive rate and or adequate security value

I do not know what elevated, high and very high should translate to. What I do know is, that it should be expressed in terms of number of false positives on a representative body of legitimate requests. But lacking such a representative body, I reckon we will simply assign them to a level we think fits and then we see how it goes.

With every level, I used a term like “adequate security value”. I do not really know what this is, but I think that there is more to assigning levels than defining false positive rate as the sole parameter. This “adequate blabla” reserves the space for clearer definitions in the future and for individual decisions on individual rules where this makes sense.

When we do stricter copies of existing rules, we can spread them over the various levels. Let me give you an example:

I described 2.2.X rule 981173 in recent a blogpost:

It triggers if a set of special characters appears 5 times in ARGS or ARGS_NAMES. This rule was removed for 3.0.0rc1. We plan to bring it back and to clone it into stricter variants. That would thus be:

Paranoia Level 0 – no 981173 rule
Paranoia Level 10 – no 981173 rule
Paranoia Level 20 – 981173 as is with an alert when limit of 5 is reached
Paranoia Level 30 – copy of 981173 with an alert when limit of 3 is reached
Paranoia Level 40 – copy of 981173 with an alert when limit of 1 is reached (-> alert on single occurrence of special character)

I think this construction is very intuitive and makes a lot of sense. So I hope to find support for this idea and this mechanism at the core of the paranoia mode. How would we configure this in terms of ModSec rule language?

Let’s look at the 3.0.0rc1 core rules. We have the following files:

REQUEST-00-LOCAL-WHITELIST.conf.example
REQUEST-01-COMMON-EXCEPTIONS.conf
REQUEST-10-IP-REPUTATION.conf
REQUEST-11-METHOD-ENFORCEMENT.conf
REQUEST-12-DOS-PROTECTION.conf
REQUEST-13-SCANNER-DETECTION.conf
REQUEST-20-PROTOCOL-ENFORCEMENT.conf
REQUEST-21-PROTOCOL-ATTACK.conf
REQUEST-30-APPLICATION-ATTACK-LFI.conf
REQUEST-31-APPLICATION-ATTACK-RFI.conf
REQUEST-32-APPLICATION-ATTACK-RCE.conf
REQUEST-33-APPLICATION-ATTACK-PHP.conf
REQUEST-41-APPLICATION-ATTACK-XSS.conf
REQUEST-42-APPLICATION-ATTACK-SQLI.conf
REQUEST-43-APPLICATION-ATTACK-SESSION-FIXATION.conf
REQUEST-49-BLOCKING-EVALUATION.conf
RESPONSE-50-DATA-LEAKAGES.conf
RESPONSE-50-DATA-LEAKAGES-IIS.conf
RESPONSE-50-DATA-LEAKAGES-JAVA.conf
RESPONSE-50-DATA-LEAKAGES-PHP.conf
RESPONSE-51-DATA-LEAKAGES-SQL.conf
RESPONSE-59-BLOCKING-EVALUATION.conf
RESPONSE-80-CORRELATION.conf

The paranoia mode candidates are spread over these files and I think they should stay in these files. Let’s not group them into a set of paranoia files. I just spoke of stricter copies of existing rules. Moving the copies away from the original is puzzling. Let’s have them reside next to the originals: I think the files should contain all rules of their category, but the paranoia rules should be skipped depending on the paranoia level.

We need to pay attention to the phases: Technically, phase 1 can appear in the rulefiles from 00 to 49. Practically it is limited to the files 00, 01 and 12. Phase 2/request is spread over the files from 00-49, and the phase 4/response is used in the files from 50 onwards. So while phase 1 and 2 can appear in the file 00 to 49, it’s mostly 2 that does. And phase 3 and 4 can appear in 50 – 59, but it is only 4 which does.

Still, I think we should prepare for what is possible and make sure we do not place a trap for future updates. This is important as it means that every rule file is passed twice (phases 1 and 2 for the request files and phase 3 and 4 for the response files). When skipping conditionally based on the paranoia level, we need to take more than one phase into consideration. It is a skip for phase 1 and a skip for phase 2 in the files targeting the request and skips for phase 3 and 4 for the response files.

This necessarily complicates things, but ignoring this need would be a inconsistent.

So how would this work in practice?

Here is an example of definition of paranoia level:

SecAction \
"id:'xxxxxx',\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:tx.paranoia_level=10"

And here is an basic layout of an updated REQUEST-42-APPLICATION-ATTACK-SQLI.conf as an example. It features 981173 brought back for paranoia level 20 and accompanied with stricter siblings on level 30 and 40.

# -= Paranoia Level 0 (empty) =- (apply unconditionally)

... no rules

SecRule TX:PARANOIA_LEVEL "@lt 10" "phase:1,id:942xxx,nolog,pass,skipAfter:END-REQUEST-42-APPLICATION-ATTACK-SQLI"
SecRule TX:PARANOIA_LEVEL "@lt 10" "phase:2,id:942xxx,nolog,pass,skipAfter:END-REQUEST-42-APPLICATION-ATTACK-SQLI"

# -= Paranoia Level 10 (default) =- (apply only when tx.paranoia_level is sufficiently high: 10 or higher)

...
... Main part of the file, most existing rules in this file
...

SecRule TX:PARANOIA_LEVEL "@lt 20" "phase:1,id:942xxx,nolog,pass,skipAfter:END-REQUEST-42-APPLICATION-ATTACK-SQLI"
SecRule TX:PARANOIA_LEVEL "@lt 20" "phase:2,id:942xxx,nolog,pass,skipAfter:END-REQUEST-42-APPLICATION-ATTACK-SQLI"

# -= Paranoia Level 20 =- (apply only when tx.paranoia_level is sufficiently high: 20 or higher)

SecRule ARGS_NAMES|ARGS|XML:/* "([\~\!\@\#\$\%\^\&\*\(\)\-\+\=\{\}\[\]\|\:\;\"\'\´\’\‘\`\<\>].*?){5,}" \
    "msg:'Restricted SQL Character Anomaly Detection Alert - Total # of special characters met/exceeded limit (5)',\
    phase:request,\
    rev:'2',\
    ver:'OWASP_CRS/3.0.0',\
    maturity:'9',\
    accuracy:'8',\
    t:none,\
    t:urlDecodeUni,\
    block,\
    id:'942xxx',\
    capture,\
    tag:'OWASP_CRS/WEB_ATTACK/SQL_INJECTION',\
    logdata:'Matched Data: %{TX.1} found within %{MATCHED_VAR_NAME}: %{MATCHED_VAR}',\
    setvar:tx.anomaly_score=+%{tx.critical_anomaly_score},\
    setvar:tx.sql_injection_score=+1,\
    setvar:'tx.msg=%{rule.msg}',\
    setvar:tx.%{rule.id}-OWASP_CRS/WEB_ATTACK/RESTRICTED_SQLI_CHARS-%{matched_var_name}=%{tx.0}"

SecRule TX:PARANOIA_LEVEL "@lt 30" "phase:1,id:942xxx,nolog,pass,skipAfter:END-REQUEST-42-APPLICATION-ATTACK-SQLI"
SecRule TX:PARANOIA_LEVEL "@lt 30" "phase:2,id:942xxx,nolog,pass,skipAfter:END-REQUEST-42-APPLICATION-ATTACK-SQLI"

# -= Paranoia Level 30 =- (apply only when tx.paranoia_level is sufficiently high: 30 or higher)

SecRule ARGS_NAMES|ARGS|XML:/* "([\~\!\@\#\$\%\^\&\*\(\)\-\+\=\{\}\[\]\|\:\;\"\'\´\’\‘\`\<\>].*?){3,}" \
    "msg:'Restricted SQL Character Anomaly Detection Alert - Total # of special characters met/exceeded limit (3)',\
    phase:request,\
    rev:'2',\
    ver:'OWASP_CRS/3.0.0',\
    maturity:'9',\
    accuracy:'8',\
    t:none,\
    t:urlDecodeUni,\
    block,\
    id:'942xxx',\
    capture,\
    tag:'OWASP_CRS/WEB_ATTACK/SQL_INJECTION',\
    logdata:'Matched Data: %{TX.1} found within %{MATCHED_VAR_NAME}: %{MATCHED_VAR}',\
    setvar:tx.anomaly_score=+%{tx.critical_anomaly_score},\
    setvar:tx.sql_injection_score=+1,\
    setvar:'tx.msg=%{rule.msg}',\
    setvar:tx.%{rule.id}-OWASP_CRS/WEB_ATTACK/RESTRICTED_SQLI_CHARS-%{matched_var_name}=%{tx.0}"

SecRule TX:PARANOIA_LEVEL "@lt 40" "phase:1,id:942xxx,nolog,pass,skipAfter:END-REQUEST-42-APPLICATION-ATTACK-SQLI"
SecRule TX:PARANOIA_LEVEL "@lt 40" "phase:2,id:942xxx,nolog,pass,skipAfter:END-REQUEST-42-APPLICATION-ATTACK-SQLI"

# -= Paranoia Level 40 =- (apply only when tx.paranoia_level is sufficiently high: 40 or higher)

SecRule ARGS_NAMES|ARGS|XML:/* "[\~\!\@\#\$\%\^\&\*\(\)\-\+\=\{\}\[\]\|\:\;\"\'\´\’\‘\`\<\>]" \
    "msg:'Restricted SQL Character Anomaly Detection Alert - Total # of special characters met/exceeded limit (0)',\
    phase:request,\
    rev:'2',\
    ver:'OWASP_CRS/3.0.0',\
    maturity:'9',\
    accuracy:'8',\
    t:none,\
    t:urlDecodeUni,\
    block,\
    id:'942xxx',\
    capture,\
    tag:'OWASP_CRS/WEB_ATTACK/SQL_INJECTION',\
    logdata:'Matched Data: %{TX.1} found within %{MATCHED_VAR_NAME}: %{MATCHED_VAR}',\
    setvar:tx.anomaly_score=+%{tx.critical_anomaly_score},\
    setvar:tx.sql_injection_score=+1,\
    setvar:'tx.msg=%{rule.msg}',\
    setvar:tx.%{rule.id}-OWASP_CRS/WEB_ATTACK/RESTRICTED_SQLI_CHARS-%{matched_var_name}=%{tx.0}"

# -= Paranoia Levels Finished =-

SecMarker "END-REQUEST-42-APPLICATION-ATTACK-SQLI"

So the idea is to add these paranoia conditions to every rule file and define an individual SecMarker at the end of every file. The skipping rules would best have the same last three digits for every file. Like 942701,942702, … and then 943701,943702,… for REQUEST-43-APPLICATION-ATTACK-SESSION-FIXATION.conf etc.

That’s all. I think the idea of various paranoia levels makes sense. I opt for integer values of 0-40, but I reckon I could live with levels from 0 to 5 as well.

Now is your turn. Please think this through and provide me with feedback. Good or bad. In fact positive feedback is really important, as I am not sure, if I am walking in the right direction and if all I get is highlighting of bad details, then it could feel as if the whole idea was bogus. So please chime in, if you agree, either via the modsecurity-core-rules mailinglist, email or twitter.

TL;DR: This post explains a possible setup for a modsec core rules paranoia level. It is a concept. I need your feedback to make it fly.

 

Christian Folini