Including OWASP ModSecurity Core Rule Set


Including the OWASP ModSecurity Core Rule Set

What are we doing?

We are embedding the OWASP ModSecurity Core Rule Set in our NGINX web server and eliminating false alarms.

Why are we doing this?

The ModSecurity Web Application Firewall, as we set up in Tutorial 6, still has barely any rules. The protection only works when you configure an additional rule set. The Core Rule Set provides generic blacklisting. This means that they inspect requests and responses for signs of attacks. The signs are often keywords or typical patterns that may be suggestive of a wide variety of attacks. This also entails false alarms (false positives) being triggered and we have to eliminate these for a successful installation.

Requirements

We will be working with the new major release of the Core Rule Set, CRS3; short for Core Rule Set 3.0. The official distribution comes with an INSTALL file that does a good job explaining the setup (after all, yours truly wrote a good deal of that file), but we will tweak the process a bit to suit our needs.

Step 1: Downloading OWASP ModSecurity Core Rule Set

The ModSecurity Core Rule Set are being developed under the umbrella of OWASP, the Open Web Application Security Project. The rules themselves are available on GitHub and can be downloaded via git or with the following wget command:

$> cd /nginx/conf
$> wget https://github.com/SpiderLabs/owasp-modsecurity-crs/archive/v3.0.2.tar.gz
$> tar xvzf v3.0.2.tar.gz
owasp-modsecurity-crs-3.0.2/
owasp-modsecurity-crs-3.0.2/CHANGES
owasp-modsecurity-crs-3.0.2/IDNUMBERING
owasp-modsecurity-crs-3.0.2/INSTALL
owasp-modsecurity-crs-3.0.2/KNOWN_BUGS
owasp-modsecurity-crs-3.0.2/LICENSE
owasp-modsecurity-crs-3.0.2/README.md
owasp-modsecurity-crs-3.0.2/crs-setup.conf.example
owasp-modsecurity-crs-3.0.2/documentation/
owasp-modsecurity-crs-3.0.2/documentation/OWASP-CRS-Documentation/
owasp-modsecurity-crs-3.0.2/documentation/README
...
$> sudo ln -s owasp-modsecurity-crs-3.0.2 /nginx/conf/crs
$> cp crs/crs-setup.conf.example crs/crs-setup.conf
$> rm v3.0.2.tar.gz

This unpacks the base part of the Core Rule Set in the directory /nginx/conf/owasp-modsecurity-crs-3.0.2. We create a link from /nginx/conf/crs to this folder. Then we copy a file named crs-setup.conf.example to a new file crs-setup.conf and finally, we delete the Core Rule Set tar file.

The setup file allows us to tweak many different settings. It is worth a look - if only to see what is included. However, we are OK with the default settings mostly and will only touch the file a little bit: We comment out the two SecDefaultAction directives in the crs-setup.conf file:

...
# Default: Anomaly Scoring mode, log to error log, log to ModSecurity audit log
# - By default, offending requests are blocked with an error 403 response.
# - To change the disruptive action, see RESPONSE-999-EXCEPTIONS.conf.example
#   and review section 'Changing the Disruptive Action for Anomaly Mode'.
# - In Apache, you can use ErrorDocument to show a friendly error page or
#   perform a redirect: https://httpd.apache.org/docs/2.4/custom-error.html
#
#SecDefaultAction "phase:1,log,auditlog,pass"
#SecDefaultAction "phase:2,log,auditlog,pass"

# Example: Anomaly Scoring mode, log only to ModSecurity audit log
...

We are doing this, because we have defined the SecDefaultAction ourselves already in the previous tutorial and ModSecurity complains if it is set multiple times.

There is one thing to note about this file. It came with the default name crs-setup.conf-example which we then renamed to crs-setup.conf. This allows us to upgrade to a future CRS release easily. We could untar the next release over the same folder without running the risk of overwriting the file we tweaked with. That does not make so much sense as our folder holds the version number in it. But let's keep it in mind for a possible future production setup.

We just make sure it is available under the new filename crs-setup.conf. Then we can continue to update the configuration to include the rules files.

Step 2: Embedding the Core Rule Set

In Tutorial 6, in which we embedded ModSecurity itself, we created a configuration file modsecurity.conf. We now add several Include directives into this section in order to load the CRS. Specifically, four parts are added to the existing configuration. (1) The Core Rules base configuration, (2) a part for self-defined rule exclusions before the Core Rules. Then (3) the Core Rules themselves with all the includes and finally a part (4) for rule exclusions after the Core Rules.

The rule exclusions are directives and rules used for managing the false alarms described above. Some false alarms must be prevented before the corresponding Core Rule is loaded. Some false alarms can only be intercepted following the definition of the core rule itself. But one thing at a time. Here is the new block of configuration which we will insert into the base configuration we assembled when we enabled ModSecurity:

# === ModSec Core Rules Base Configuration (ids: 900000-900999)

Include /nginx/conf/crs/crs-setup.conf

SecAction "id:900110,phase:1,pass,nolog,\
  setvar:tx.inbound_anomaly_score_threshold=5,\
  setvar:tx.outbound_anomaly_score_threshold=5"

SecAction "id:900000,phase:1,pass,nolog,\
  setvar:tx.paranoia_level=1"


# === ModSec Core Rules: Runtime Exclusion Rules (ids: 10000-49999)

# ...


# === ModSecurity Core Rules Inclusion


Include /nginx/conf/crs/rules/REQUEST-901-INITIALIZATION.conf
Include /nginx/conf/crs/rules/REQUEST-903.9001-DRUPAL-EXCLUSION-RULES.conf
Include /nginx/conf/crs/rules/REQUEST-903.9002-WORDPRESS-EXCLUSION-RULES.conf
Include /nginx/conf/crs/rules/REQUEST-905-COMMON-EXCEPTIONS.conf
Include /nginx/conf/crs/rules/REQUEST-910-IP-REPUTATION.conf
Include /nginx/conf/crs/rules/REQUEST-911-METHOD-ENFORCEMENT.conf
Include /nginx/conf/crs/rules/REQUEST-912-DOS-PROTECTION.conf
Include /nginx/conf/crs/rules/REQUEST-913-SCANNER-DETECTION.conf
Include /nginx/conf/crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf
Include /nginx/conf/crs/rules/REQUEST-921-PROTOCOL-ATTACK.conf
Include /nginx/conf/crs/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf
Include /nginx/conf/crs/rules/REQUEST-931-APPLICATION-ATTACK-RFI.conf
Include /nginx/conf/crs/rules/REQUEST-932-APPLICATION-ATTACK-RCE.conf
Include /nginx/conf/crs/rules/REQUEST-933-APPLICATION-ATTACK-PHP.conf
Include /nginx/conf/crs/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf
Include /nginx/conf/crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf
Include /nginx/conf/crs/rules/REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION.conf
Include /nginx/conf/crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf
Include /nginx/conf/crs/rules/RESPONSE-950-DATA-LEAKAGES.conf
Include /nginx/conf/crs/rules/RESPONSE-951-DATA-LEAKAGES-SQL.conf
Include /nginx/conf/crs/rules/RESPONSE-952-DATA-LEAKAGES-JAVA.conf
Include /nginx/conf/crs/rules/RESPONSE-953-DATA-LEAKAGES-PHP.conf
Include /nginx/conf/crs/rules/RESPONSE-954-DATA-LEAKAGES-IIS.conf
Include /nginx/conf/crs/rules/RESPONSE-959-BLOCKING-EVALUATION.conf
Include /nginx/conf/crs/rules/RESPONSE-980-CORRELATION.conf


# === ModSec Core Rules: Startup Time Rules Exclusions

# ...

The Core Rule Set comes with a base configuration file named crs-setup.conf which we prepared during the installation. Copying the original example file guarantees that we can update the Core Rule Set distribution without harming our copy of the config file unless we want to.

We have the option to edit settings in that base configuration file. However, the strategy for this series of tutorials has been to define all the important things close to our single NGINX configuration file. We do not want to insert the complete contents of the crs-setup.conf file into our configuration (but we include it) in order to get the minimal set of configuration items needed to run the Core Rules. I do not want to dive into all the options in the settings file, but it is worth having a look at.

For now, we leave the crs-setup.conf file untouched, but we take three important values out of crs-setup.conf and define them in our config so we have them in sight at all times. We define two thresholds in the unconditional rule 900110: The inbound anomaly score and the outbound anomaly score. This is done via the setvar action which sets both values to 5, which also happens to be the default value. But that way, we really know the setting as it is in plain sight.

What does that mean? The Core Rule Set works with a scoring mechanism by default. For every rule a request violates, there is a score being raised. When all the request rules have passed, the score is compared to the limit. If if hits the limit, the request is blocked. The same thing happens with the responses, where we want to avoid information leaks to the client.

The Core Rule Set comes in blocking mode by default. If a rule is violated and the score hits the limit, the blocking will be effective immediately. And with a anomaly threshold of 5, this is effectively the case. However, if we were to move this into production immediately, we would raise the threshold in order to avoid any false positives before we had the chance to handle them for the service.

The second rule, id 900000, defines the Paranoia Level to 1. The Core Rules are divided in four groups at paranoia levels 1 - 4. As the name suggests, the higher the paranoia level, the more paranoid the rules. The default is paranoia level 1, where the rules are quite sane and false alarms are rare. When you raise the PL to 2, additional rules are enabled. Starting with PL 2, you will face more and more false alarms, also called false positives. This number grows with PL3 and when you arrive at PL4, you are likely to face false alarms as though your web application firewall has become quite paranoid, so to speak. We will deal with false positives later in this tutorial, but for the moment you just need to be aware that you can control the aggressiveness of the rule set with the paranoia level setting and that PL3 and PL4 are really for advanced users with very high security needs.

Step 3: A closer look at the rules folder

The center of the previous config snippet follows the include statement, which loads all files with suffix .conf from the rules sub folder in the CRS directory. This is where all the rules are being loaded. Let's take a look at them:

$> ls -1 *.conf
crs/rules/REQUEST-901-INITIALIZATION.conf
crs/rules/REQUEST-903.9001-DRUPAL-EXCLUSION-RULES.conf
crs/rules/REQUEST-903.9002-WORDPRESS-EXCLUSION-RULES.conf
crs/rules/REQUEST-905-COMMON-EXCEPTIONS.conf
crs/rules/REQUEST-910-IP-REPUTATION.conf
crs/rules/REQUEST-911-METHOD-ENFORCEMENT.conf
crs/rules/REQUEST-912-DOS-PROTECTION.conf
crs/rules/REQUEST-913-SCANNER-DETECTION.conf
crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf
crs/rules/REQUEST-921-PROTOCOL-ATTACK.conf
crs/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf
crs/rules/REQUEST-931-APPLICATION-ATTACK-RFI.conf
crs/rules/REQUEST-932-APPLICATION-ATTACK-RCE.conf
crs/rules/REQUEST-933-APPLICATION-ATTACK-PHP.conf
crs/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf
crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf
crs/rules/REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION.conf
crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf
crs/rules/RESPONSE-950-DATA-LEAKAGES.conf
crs/rules/RESPONSE-951-DATA-LEAKAGES-SQL.conf
crs/rules/RESPONSE-952-DATA-LEAKAGES-JAVA.conf
crs/rules/RESPONSE-953-DATA-LEAKAGES-PHP.conf
crs/rules/RESPONSE-954-DATA-LEAKAGES-IIS.conf
crs/rules/RESPONSE-959-BLOCKING-EVALUATION.conf
crs/rules/RESPONSE-980-CORRELATION.conf

The rule files are grouped by request and response rules. We start off with an initialization rule file. There are a lot of things commented out in the crs-setup.conf file. These values are simply set to their default value in the 901 rule file. This helps keep the config neat and tidy and still have all default settings applied. Then we have two application specific rule files for Wordpress and Drupal, followed by an exceptions file that is mostly irrelevant to us. Starting with 910, we have the real rules.

Every file is dedicated to a topic or type of attack. The Core Rule Set occupy the ID namespace from 900,000 to 999,999. The first three digits of every rule correspond to three digits in the rule files. This means the IP reputation rules in REQUEST-910-IP-REPUTATION.conf will occupy the rule range 910,000 - 910,999. The method enforcement rules follow between 911,000 and 911,999, etc.. Some of these rule files are small and they do not use up their assigned rule range by far. Others are much bigger and the infamous SQL Injection rules run the risk of touching their ID ceiling one day.

An important rule file is REQUEST-949-BLOCKING-EVALUATION.conf. This is where the anomaly score is checked against the inbound threshold and the request is blocked accordingly.

Then begin the outbound rules, which are less numerous and basically check for code leakages (stack traces!) and leakages in error messages (which give an attacker useful information to construct an SQL injection attack). The outbound score is checked in the file with the 959 prefix, and then again in the file with the 980 prefix.

Some of the rules come with data files. These files have a .data extension and reside in the same folder with the rule files. Data files are typically used when the request has to be checked against a long list of keywords, like unwanted user agents or php function names. Have a look if you are interested.

Before and after the rules Include directive in our NGINX configuration file, there is a bit of configuration space reserved. This is where we will be handling false alarms in the future. Some of them are being treated before the rules are loaded in the configuration, some after the Include directive. We'll return to this later in this tutorial.

Step 4: Tweaking the Rule Set and Final Configuration

Having looked at the rules, let's tweak the setup a bit. The file RESPONSE-980-CORRELATION.conf performs some statistical reporting when a request hits at least one of the rules. But we want to put these requests into a wider perspective, so we also want to learn about requests not triggering any rules. This is useful, because it gives us statistical context and thus an overview over the traffic and the quality of our WAF configuration. So we introduce a rule with the ID 980145 ourselves, in order to report the anomaly scores of all the requests.

SecAction "id:980145,phase:5,pass,t:none,log,noauditlog,\
    msg:'Incoming and Outgoing Score: %{TX.ANOMALY_SCORE} %{TX.OUTBOUND_ANOMALY_SCORE}'"

# === ModSec Core Rules: Startup Time Rules Exclusions

# ModSecurity Rule Excludsion: 980130 Suppress statistics for blocked requests by rule 980130
#    (-> replaced by 980145, that we wrote ourselved)
SecRuleRemoveById 980130

This makes the rule performing this for suspicious requests, 980130, redundant and we remove that rule from the rule set with the help of the SecRuleRemoveById directive.

For completeness, here is the complete ModSecurity modsecurity.conf that is being included by ngxin.conf. This loads and configures ModSecurity, the Core Rules and all the other config bits from the previous tutorial.

SecRuleEngine On
SecRequestBodyAccess On
SecRequestBodyLimit 13107200

SecRequestBodyNoFilesLimit 64000

SecResponseBodyAccess On
SecResponseBodyLimit 10000000

SecTmpDir /tmp/
SecDataDir /tmp/
SecUploadDir /tmp/

SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
SecAuditLogParts ABEFHIJZ

SecAuditLogType Serial
SecAuditLog logs/modsec_audit.log
SecAuditLogStorageDir logs/audit

SecPcreMatchLimit 500000
SecPcreMatchLimitRecursion 500000

SecDebugLog logs/modsec_debug.log
SecDebugLogLevel 0


# == ModSec Rule ID Namespace Definition
# Service-specific before Core-Rules:    10000 -  49999
# Service-specific after Core-Rules:     50000 -  79999
# Locally shared rules:                  80000 -  99999
# Recommended ModSec Rules (few):       200000 - 200010
# OWASP Core-Rules:                     900000 - 999999


# === ModSec Recommended Rules (in modsec src package) (ids: 200000-200010)

SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\+|/)|text/)xml" \
  "id:200000,phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML"

SecRule REQUEST_HEADERS:Content-Type "application/json" \
  "id:200001,phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"

SecRule REQBODY_ERROR "!@eq 0" \
  "id:200002,phase:2,t:none,deny,status:400,log,\
  msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2"

SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
  "id:200003,phase:2,t:none,deny,status:403,log, \
  msg:'Multipart request body failed strict validation: \
  PE %{REQBODY_PROCESSOR_ERROR}, \
  BQ %{MULTIPART_BOUNDARY_QUOTED}, \
  BW %{MULTIPART_BOUNDARY_WHITESPACE}, \
  DB %{MULTIPART_DATA_BEFORE}, \
  DA %{MULTIPART_DATA_AFTER}, \
  HF %{MULTIPART_HEADER_FOLDING}, \
  LF %{MULTIPART_LF_LINE}, \
  SM %{MULTIPART_MISSING_SEMICOLON}, \
  IQ %{MULTIPART_INVALID_QUOTING}, \
  IP %{MULTIPART_INVALID_PART}, \
  IH %{MULTIPART_INVALID_HEADER_FOLDING}, \
  FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'"

SecRule TX:/^MSC_/ "!@streq 0" \
  "id:200005,phase:2,t:none,deny,status:500,\
  msg:'ModSecurity internal error flagged: %{MATCHED_VAR_NAME}'"


# === ModSecurity Rules 
#

# === ModSec Core Rules Base Configuration (ids: 900000-900999)

Include /nginx/conf/crs/crs-setup.conf

SecAction "id:900110,phase:1,pass,nolog,\
  setvar:tx.inbound_anomaly_score_threshold=5,\
  setvar:tx.outbound_anomaly_score_threshold=5"

SecAction "id:900000,phase:1,pass,nolog,\
  setvar:tx.paranoia_level=1"


# === ModSec Core Rules: Runtime Exclusion Rules (ids: 10000-49999)

# ...


# === ModSecurity Core Rules Inclusion

Include /nginx/conf/crs/rules/REQUEST-901-INITIALIZATION.conf
Include /nginx/conf/crs/rules/REQUEST-903.9001-DRUPAL-EXCLUSION-RULES.conf
Include /nginx/conf/crs/rules/REQUEST-903.9002-WORDPRESS-EXCLUSION-RULES.conf
Include /nginx/conf/crs/rules/REQUEST-905-COMMON-EXCEPTIONS.conf
Include /nginx/conf/crs/rules/REQUEST-910-IP-REPUTATION.conf
Include /nginx/conf/crs/rules/REQUEST-911-METHOD-ENFORCEMENT.conf
Include /nginx/conf/crs/rules/REQUEST-912-DOS-PROTECTION.conf
Include /nginx/conf/crs/rules/REQUEST-913-SCANNER-DETECTION.conf
Include /nginx/conf/crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf
Include /nginx/conf/crs/rules/REQUEST-921-PROTOCOL-ATTACK.conf
Include /nginx/conf/crs/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf
Include /nginx/conf/crs/rules/REQUEST-931-APPLICATION-ATTACK-RFI.conf
Include /nginx/conf/crs/rules/REQUEST-932-APPLICATION-ATTACK-RCE.conf
Include /nginx/conf/crs/rules/REQUEST-933-APPLICATION-ATTACK-PHP.conf
Include /nginx/conf/crs/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf
Include /nginx/conf/crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf
Include /nginx/conf/crs/rules/REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION.conf
Include /nginx/conf/crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf
Include /nginx/conf/crs/rules/RESPONSE-950-DATA-LEAKAGES.conf
Include /nginx/conf/crs/rules/RESPONSE-951-DATA-LEAKAGES-SQL.conf
Include /nginx/conf/crs/rules/RESPONSE-952-DATA-LEAKAGES-JAVA.conf
Include /nginx/conf/crs/rules/RESPONSE-953-DATA-LEAKAGES-PHP.conf
Include /nginx/conf/crs/rules/RESPONSE-954-DATA-LEAKAGES-IIS.conf
Include /nginx/conf/crs/rules/RESPONSE-959-BLOCKING-EVALUATION.conf
Include /nginx/conf/crs/rules/RESPONSE-980-CORRELATION.conf

SecAction "id:980145,phase:5,pass,t:none,log,noauditlog,\
    msg:'Incoming and Outgoing Score: %{TX.ANOMALY_SCORE} %{TX.OUTBOUND_ANOMALY_SCORE}'"


# === ModSec Core Rules: Startup Time Rules Exclusions

# ModSecurity Rule Excludsion: 980130 Suppress statistics for blocked requests by rule 980130
#    (-> replaced by 980145, that we wrote ourselved)
SecRuleRemoveById 980130

# ...

We have embedded the Core Rule Set and are now ready for a test operation. The rules inspect requests and responses. They will trigger alarms if they encounter fishy requests, but they will not block any transaction, because the limits have been set very high. Let's give it a shot.

Step 5: Triggering alarms for testing purposes

For starters, we will do something easy. It is a request that will trigger exactly one rule by attempting to execute a bash shell. We know that our simple lab server is not vulnerable to such a blatant attack, but ModSecurity does not know this and will still try to protect us:

$> curl localhost/index.html?exec=/bin/bash
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>

As predicted, we are being blocked, but let's check the logs to see more about what happened:

$> tail -1 /nginx/logs/access.log
127.0.0.1 - - [11/Mar/2018:09:42:46 +0100] "GET /index.html?exec=/bin/bash HTTP/1.1" 403 162 "-" "curl/7.47.0"

It looks like a malicious GET request with a status 403. Usually, we would want to add a lot more interesting information into the access log. But NGINX is not very flexible in this regard; namely when compared to Apache that can display any information available to the server. So for NGINX we need to rely more on the Error Log. That's why we introduced the rule 980145 above in order to display the anomaly scores of the requests. Said scores would be better suited for the access log, but that's not possible with NGINX as of this writing and the Error Log solution can be made to work just as well as we can see here:

2018/03/11 10:42:30 [info] 4959#4959: *2 ModSecurity: Warning. Matched "Operator `PmFromFile' with parameter `unix-shell.data' against variable `ARGS:exec' (Value: `/bin/bash' ) [file "/nginx/conf/crs/rules/REQUEST-932-APPLICATION-ATTACK-RCE.conf"] [line "404"] [id "932160"] [rev "1"] [msg "Remote Command Execution: Unix Shell Code Found"] [data "Matched Data: bin/bash found within ARGS:exec: /bin/bash"] [severity "2"] [ver "OWASP_CRS/3.0.0"] [maturity "1"] [accuracy "8"] [tag "application-multi"] [tag "language-shell"] [tag "platform-unix"] [tag "attack-rce"] [tag "OWASP_CRS/WEB_ATTACK/COMMAND_INJECTION"] [tag "WASCTC/WASC-31"] [tag "OWASP_TOP_10/A1"] [tag "PCI/6.5.2"] [hostname "127.0.0.1"] [uri "/index.html"] [unique_id "152076135035.314680"] [ref "o1,8v21,9t:urlDecodeUni,t:cmdLine,t:normalizePath,t:lowercase"], client: 127.0.0.1, server: localhost, request: "GET /index.html?exec=/bin/bash HTTP/1.1", host: "localhost"
2018/03/11 10:42:30 [info] 4959#4959: *2 ModSecurity: Access denied with code %d (phase 2). Matched "Operator `Ge' with parameter `%{tx.inbound_anomaly_score_threshold}' against variable `TX:ANOMALY_SCORE' (Value: `5' ) [file "/nginx/conf/crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "36"] [id "949110"] [rev ""] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [data ""] [severity "2"] [ver ""] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [hostname "127.0.0.1"] [uri "/index.html"] [unique_id "152076135035.314680"] [ref ""], client: 127.0.0.1, server: localhost, request: "GET /index.html?exec=/bin/bash HTTP/1.1", host: "localhost"
2018/03/11 10:42:30 [warn] 4959#4959: *2 [client 127.0.0.1] ModSecurity: Warning. Matched "Operator `Ge' with parameter `%{tx.inbound_anomaly_score_threshold}' against variable `TX:ANOMALY_SCORE' (Value: `5' ) [file "/nginx/conf/crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "36"] [id "949110"] [rev ""] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [data ""] [severity "2"] [ver ""] [maturity "0"] [accuracy "0"] [hostname "127.0.0.1"] [uri "/index.html"] [unique_id "152076135035.314680"] [ref ""], client: 127.0.0.1, server: localhost, request: "GET /index.html?exec=/bin/bash HTTP/1.1", host: "localhost"
2018/03/11 10:42:30 [info] 4959#4959: *2 ModSecurity: Warning.  [file "conf/modsecurity.conf"] [line "117"] [id "980145"] [rev ""] [msg "Incoming and Outgoing Score: 5 0"] [data ""] [severity "0"] [ver ""] [maturity "0"] [accuracy "0"] [hostname "127.0.0.1"] [uri "/index.html"] [unique_id "152076135035.314680"] [ref ""] while logging request, client: 127.0.0.1, server: localhost, request: "GET /index.html?exec=/bin/bash HTTP/1.1", host: "localhost"

Let's look at the last line first. The message of the rule reads: Incoming and Outgoing Score: 5 0. So this request scored an incoming anomaly score of 5 by triggering a single critical rule. We can confirm this by looking at the first line and the data field: Matched Data: bin/bash found within ARGS:exec: /bin/bash. The remaining two lines in the center are the rule 949110 being reported twice (a glitch in ModSecurity 3.0) for blocking the request, because it met the anomaly scoring threshold defined as 5.

So 5 is a critical violation. What other scores do exist? An error level violation is set at 4, a warning at 3 and a notice at 2. However, if you look over the rules in all the files, most of them score as critical violations with a score of 5.

Let's give the alerts a closer look. The beginning of the line consists of the NGINX-specific parts such as the timestamp and the severity of the message as the NGINX server sees it. ModSecurity messages are set to info or warn level. The part of the message writting by ModSecurity starts with that keyword. We see three times ModSecurity: Warning and once ModSecurity: Access denied. The characteristic marker of a Core Rule Set alert is ModSecurity: Warning. It describes a rule being triggered without blocking the request. This is because the alert only raised the anomaly score. It is very easy to distinguish between the issuing of alarms and actual blocking in the NGINX error log. Particularly since the individual Core Rules increase the anomaly score, but they do not trigger a blockade. The blockade itself is performed by a separate blocking rule taking the limit into account and then blocking in 949110 with ModSecurity: Access denied.

What comes next in an alert message? A reference to the pattern found in the request. The specific phrase /bin/bash was found in the argument exec. Then comes a series of information chunks that always have the same pattern: They are within square brackets and have their own identifier. First you'll see the file identifier. It shows us the file in which the rule that triggered the alarm is defined. This is followed by line for the line number within the file. The id parameter is an important one. The rule in question, 932160, can be found in the set of rules that defend against remote command execution in the 932,000 - 932,999 rule block. Then comes rev as a reference to the revision number of the rule. In Core Rules, this parameter expresses how often the rule has been revised. If a modification is made to a rule, rev increases by one. msg, short for message, describes the type of attack detected. The relevant part of the request, the exec parameter appears in data. In my example, this is obviously a case of Remote Code Execution (RCE).

Then we have the severity level of the rule that set off the alarm and corresponds with the anomaly score of the rule. We have already established the fact that our rule is considered critical, that's why it is being reported here at this severity. At ver, we come to the release of the core rule set, followed by maturity and then accuracy. Both values are meant to be references to the quality of the rule. But the support is in fact inconsistent and you should not trust these values very much.

What follows is a series of tags assigned to the rule. They are included along with every alert message. These tags often classify the type of attack. These references can, for example, be used for analysis and statistics. Towards the end of the alarm comes three additional values, hostname, uri and unique_id, that more clearly specify the request.

With this, we have covered the full alert message that led to the inbound anomaly score of 5. That was only a single request with a single alert. Let's generate more alerts. Nikto is a simple tool that can help us in this situation. It's a security scanner that has been around for ages. It's not very proficient, but it is fast and easy to use. Just the right tool to generate alerts for us. Nikto may still have to be installed. The scanner is, however, included in most distributions.

$> nikto -h localhost
- Nikto v2.1.5
---------------------------------------------------------------------------
+ Target IP:          127.0.0.1
+ Target Hostname:    localhost
+ Target Port:        80
+ Start Time:         2018-03-11 11:14:15 (GMT1)
---------------------------------------------------------------------------
+ Server: nginx
+ The anti-clickjacking X-Frame-Options header is not present.
+ No CGI Directories found (use '-C all' to force check all possible dirs)

This scan should have triggered numerous ModSecurity alarms on the server. Let’s take a close look at the NGINX error log. In my case, there were over 30,000 entries in the error log. Combine this with the authorization messages and infos on many 404s (Nikto probes for files that do not exist on the server) and you end up with a fast-growing error log. The single Nikto run resulted in a 25 MB8.8 MB logfile. Looking over the audit log tree reveals 78 MB of logs. It's obvious: you need to keep a close eye on these log files or your server will collapse due to denial of service via log file exhaustion.

Step 6: Analyzing the alert messages

So we are looking at 7,300 alerts. And even if the format of the entries in the error log may be clear, without a tool they are very hard to read, let alone analyze. A simple remedy is to use a few shell aliases, which extract individual pieces of information from the entries. They are stored in the alias file we discussed in the log format in Tutorial 5.

$> cat ~/.nginx-modsec.alias
...
alias meldata='grep -o "\[data [^]]*" | cut -d\" -f2'
alias melfile='grep -o "\[file [^]]*" | cut -d\" -f2'
alias melhostname='grep -o "\[hostname [^]]*" | cut -d\" -f2'
alias melid='grep -o "\[id [^]]*" | cut -d\" -f2'
alias melidmsg='grep -o "\[id [^]]*\].*\[msg [^]]*\]" | sed -e "s/\].*\[/] [/" -e "s/\[msg //" | cut -d\  -f2- | tr -d "\]\"" | sed -e "s/(Total .*/(Total ...) .../" -e "s/Incoming and Outgoing Score: [0-9]* [0-9]*/Incoming and Outgoing Score: .../"'
alias melip='grep -o "\[client [^]]*" | cut -b9-'
alias melline='grep -o "\[line [^]]*" | cut -d\" -f2'
alias melmatch='grep -o " at [^\ ]*\. \[file" | sed -e "s/\. \[file//" | cut -b5-'
alias melmsg='grep -o "\[msg [^]]*" | cut -d\" -f2 | sed -e "s/(Total .*/(Total ...) .../"'
alias melsummary='grep -o -E " (at|against) .*\[file.*\[id \"[0-9]+.*\[msg \"[^\"]+" | tr -d \" | sed -e "s/ at the end of input at/ at/" -e "s/ required. /. /" -e "s/\[rev .*\[msg/[msg/" -e "s/\. / /" -e "s/(Total .*/(Total ...) .../" | tr -d \] | cut -d\  -f3,9,11- | sed -e "s/^\([^ ]*\) \([^ ]*\)/\2 \1/" | awk "{ printf \"%+6s %-35s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s\n\", \$1, \$2, \$3, \$4, \$5, \$6, \$7, \$8, \$9, \$10, \$11, \$12, \$13, \$14, \$15, \$16, \$17, \$18, \$19, \$20 }" | sed -e "s/\ *$//"'
alias meltags='tr "]" "\n" | tr "[" "\n" | grep "tag \"" | cut -b6- | tr -d "\""'
alias meltimestamp='cut -b2-25'
alias melunique_id='grep -o "\[unique_id [^]]*" | cut -d\" -f2'
alias meluri='grep -o "\[uri [^]]*" | cut -d\" -f2'
...
$> source ~/.nginx-modsec.alias 

These abbreviations all start with the prefix mel, short for ModSecurity error log, followed by the field name. Let’s try it out to output the rule IDs from the messages:

$> cat logs/error.log | melid | tail
980145
913100
949110
949110
980145
913100
920440
949110
949110
980145

This seems to do the job. So let’s extend the example a few steps:

$> cat logs/error.log | melid | sort | uniq -c | sort -n
      1 920220
      1 932115
      2 920280
      2 942100
      3 942270
      4 933150
      4 941140
      6 911100
      6 932110
     10 920100
     13 932100
     13 932105
     16 941170
     17 920170
     18 930130
     38 932150
     67 920270
     67 933130
     70 933160
    111 941180
    138 932160
    141 931110
    190 930100
    204 930110
    218 930120
    225 920440
    236 941110
    239 941100
    241 941160
   2276 931120
   2342 913120
   6251 913100
   6251 980145
  12502 949110
$> cat logs/error.log | melid | sort | uniq -c | sort -n | while read STR; do echo -n "$STR "; \
ID=$(echo "$STR" | sed -e "s/.*\ //"); grep $ID logs/error.log | head -1 | melmsg; done
1 920220 URL Encoding Abuse Attack Attempt
1 932115 Remote Command Execution: Windows Command Injection
2 920280 Request Missing a Host Header
2 942100 SQL Injection Attack Detected via libinjection
3 942270 Looking for basic sql injection. Common attack string for mysql, oracle and others.
4 933150 PHP Injection Attack: High-Risk PHP Function Name Found
4 941140 XSS Filter - Category 4: Javascript URI Vector
6 911100 Method is not allowed by policy
6 932110 Remote Command Execution: Windows Command Injection
10 920100 Invalid HTTP Request Line
13 932100 Remote Command Execution: Unix Command Injection
13 932105 Remote Command Execution: Unix Command Injection
16 941170 NoScript XSS InjectionChecker: Attribute Injection
17 920170 GET or HEAD Request with Body Content.
18 930130 Restricted File Access Attempt
38 932150 Remote Command Execution: Direct Unix Command Execution
67 920270 Invalid character in request (null character)
67 933130 PHP Injection Attack: Variables Found
70 933160 PHP Injection Attack: High-Risk PHP Function Call Found
111 941180 Node-Validator Blacklist Keywords
138 932160 Remote Command Execution: Unix Shell Code Found
141 931110 Possible Remote File Inclusion (RFI) Attack: Common RFI Vulnerable Parameter Name used w/URL Payload
190 930100 Path Traversal Attack (/../)
204 930110 Path Traversal Attack (/../)
218 930120 OS File Access Attempt
225 920440 URL file extension is restricted by policy
236 941110 XSS Filter - Category 1: Script Tag Vector
239 941100 XSS Attack Detected via libinjection
241 941160 NoScript XSS InjectionChecker: HTML Injection
2276 931120 Possible Remote File Inclusion (RFI) Attack: URL Payload Used w/Trailing Question Mark Character (?)
2342 913120 Found request filename/argument associated with security scanner
6251 913100 Found User-Agent associated with security scanner
6251 980145 Incoming and Outgoing Score: 5 0
12502 949110 Inbound Anomaly Score Exceeded (Total ...) ...

This, we can work with. But it’s perhaps necessary to explain the one-liners. We extract the rule IDs from the error log, then sort them, sum them together in a list of found IDs (uniq -c) and sort again by the numbers found. That’s the first one-liner. A relationship between the individual rules is still lacking, because there’s not much we can do with the ID number yet. We get the names from the error log again by looking through the previously run test line-by-line in a loop. We out the ID that we have into this loop ($STR). Then we have to separate the number of found items and the IDs again. This is done using an embedded sub-command (ID=$(echo "$STR" | sed -e "s/.*\ //")). We then use the IDs we just found to search the error log once more for an entry, but take only the first one, extract the msg part and display it. Done.

You might now think that it would be better to define an additional alias to determine the ID and description of the rule in a single step. This puts us on the wrong path, though, because there are rules that contain dynamic parts in and following the brackets (anomaly scores in the rules checking the threshold with rule ID 949110 and 980145!). We, of course, want to combine these rules, putting them together in order to map the rule only once. So, to really simplify analysis, we have to get rid of the dynamic items. Here’s an additional alias, that is also part of the .nginx-modsec.alias file, that implements this idea:

alias melidmsg='grep -o "\[id [^]]*\].*\[msg [^]]*\]" | sed -e "s/\].*\[/] [/" \
-e "s/\[msg //" | cut -d\  -f2- | tr -d "\]\"" | sed -e "s/(Total .*/(Total ...) .../" \
-e "s/Incoming and Outgoing Score: [0-9]* [0-9]*/Incoming and Outgoing Score: .../"'
$> cat logs/error.log | melidmsg | sucs
      1 920220 URL Encoding Abuse Attack Attempt
      1 932115 Remote Command Execution: Windows Command Injection
      2 920280 Request Missing a Host Header
      2 942100 SQL Injection Attack Detected via libinjection
      3 942270 Looking for basic sql injection. Common attack string for mysql, oracle and others.
      4 933150 PHP Injection Attack: High-Risk PHP Function Name Found
      4 941140 XSS Filter - Category 4: Javascript URI Vector
      6 911100 Method is not allowed by policy
      6 932110 Remote Command Execution: Windows Command Injection
     10 920100 Invalid HTTP Request Line
     13 932100 Remote Command Execution: Unix Command Injection
     13 932105 Remote Command Execution: Unix Command Injection
     16 941170 NoScript XSS InjectionChecker: Attribute Injection
     17 920170 GET or HEAD Request with Body Content.
     18 930130 Restricted File Access Attempt
     38 932150 Remote Command Execution: Direct Unix Command Execution
     67 920270 Invalid character in request (null character)
     67 933130 PHP Injection Attack: Variables Found
     70 933160 PHP Injection Attack: High-Risk PHP Function Call Found
    111 941180 Node-Validator Blacklist Keywords
    138 932160 Remote Command Execution: Unix Shell Code Found
    141 931110 Possible Remote File Inclusion (RFI) Attack: Common RFI Vulnerable Parameter Name used w/URL Payload
    190 930100 Path Traversal Attack (/../)
    204 930110 Path Traversal Attack (/../)
    218 930120 OS File Access Attempt
    225 920440 URL file extension is restricted by policy
    236 941110 XSS Filter - Category 1: Script Tag Vector
    239 941100 XSS Attack Detected via libinjection
    241 941160 NoScript XSS InjectionChecker: HTML Injection
   2276 931120 Possible Remote File Inclusion (RFI) Attack: URL Payload Used w/Trailing Question Mark Character (?)
   2342 913120 Found request filename/argument associated with security scanner
   6251 913100 Found User-Agent associated with security scanner
   6251 980145 Incoming and Outgoing Score: ...
  12502 949110 Inbound Anomaly Score Exceeded (Total ...) ...

So that's something we can work with. It shows that the Core Rules detected a lot of malicious requests and we now have an idea which rules played a role in this. The non-statistical rules that triggered most frequently, 913100 and 913120, are no surprise, and when you look upwards in the output, this all makes a lot of sense.

Step 7: First Encounter with False Alerts

So the Nikto scan set off thousands of alarms. They were likely justified. In the normal use of ModSecurity, things are a bit different. The Core Rule Set is designed and optimized to have as few false alarms as possible in paranoia level 1. But in production use, there are going to be false positives sooner or later. Depending on the application, a normal installation will also see alarms and a lot of them will be false. And when you raise the paranoia level to become more vigilant towards attacks, the number of false positives will also rise. Actually, it will rise steeply when you move to PL 3 or 4; so steeply, some would call it exploding.

In order to run smoothly, the configuration has to be fine tuned first. Legitimate requests and exploitation attempts need to be distinct. We want to achieve a high degree of separation between the two. We wish to configure ModSecurity and the CRS so the engine knows exactly how to distinguish between legitimate requests and attacks.

False alarms are possible in both directions. Attacks that are not detected are called false negatives. The Core Rules are strict and careful to keep the number of false negatives low. An attacker needs to possess a great deal of savvy to circumvent the system of rules, especially at higher paranoia levels. Unfortunately, this strictness also results in alarms being triggered for normal requests. It is commonly the case that at a low degree of separation, you either get a lot of false negatives or a lot of false positives. Reducing the number of false negatives leads to an increase in false positives and vice versa. Both correlate highly with one another.

We have to overcome this link: We want to increase the degree of separation in order to reduce the number of false positives without increasing the number of false negatives. We can do this by fine tuning the system of rules in a few places. We have to exclude certain rules from being executed for certain requests or parameters. Let's look at this in details and let's generate a false positive:

$> curl localhost/login/Login.do --data "password=k1ck-bin/bash"
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>

There is nothing really wrong with this password outside of the fact, that the CRS thinks it's a remote command execution attempt:

$> grep 980145 logs/error.log | tail -1 
2018/03/11 12:48:25 [info] 6071#6071: *36 ModSecurity: Warning.  [file "conf/modsecurity.conf"] [line "117"] [id "980145"] [rev ""] [msg "Incoming and Outgoing Score: 5 0"] [data ""] [severity "0"] [ver ""] [maturity "0"] [accuracy "0"] [hostname "127.0.0.1"] [uri "/login/Login.do"] [unique_id "152076890543.106174"] [ref ""] while logging request, client: 127.0.0.1, server: localhost, request: "POST /login/Login.do HTTP/1.1", host: "localhost"
$> grep 980145 logs/error.log | tail -1 | melunique_id 
152076890543.106174
$> grep 152076890543.106174 logs/error.log | melidmsg
932160 Remote Command Execution: Unix Shell Code Found
949110 Inbound Anomaly Score Exceeded (Total ...) ...
949110 Inbound Anomaly Score Exceeded (Total ...) ...
980145 Incoming and Outgoing Score: ...

Here we first identified the last request in the log file, we extracted the unique id of the request and then filtered the complete error log for this request, extracting the rule ids and the message of the alerts. We still see the duplication of 941110, but more interestingly, we see that the suspicious password triggered 932160. It does not take much thinking why that might be case. Let's look:

$> grep 152076890543.106174 logs/error.log | grep 932160 | meldata
Matched Data: bin/bash found within ARGS:password: k1ck-bin/bash

Clearly a false positive that we want to get rid of. But how do we achieve this and what are our options?

Step 8: Handling false positives: Disabling individual rules

The simple way of dealing with a false positive is to simply disable the rule. We are thus making the alarm disappear by excluding a certain rule from the rule set. The CRS term for this technique is called Rules Exclusion or Exclusion Rules. It is called Rule because this exclusion involved writing rules or directives resembling rules themselves.

Excluding a rule completely takes very little effort, but it is, of course, potentially risky because the rule is not being disabled for just legitimate users, but for attackers as well. By completely disabling a rule, we are restricting the capability of ModSecurity. Or, expressed more drastically, we’re pulling the teeth out of the WAF.

Let's try this out for the sake of this exercise. We add the following two lines to the final section of modsecurity.conf:

# ModSec Rule Exclusion: 932160 : Remote Command Execution: Unix Shell Code Found
SecRuleRemoveById 932160

In fact, we had used SecRuleRemoveById before when we axed 980130, and here we do the very same: We remove 932160 from the complete rule set for the complete server. It stops to exist for our installation; a measure that far too broad to be used in production for this rule but it's not yet the worst. We can also remove it by tag.

$> grep 152076890543.106174 logs/error.log | grep 932160 | meltags
application-multi
language-shell
platform-unix
attack-rce
OWASP_CRS/WEB_ATTACK/COMMAND_INJECTION
WASCTC/WASC-31
OWASP_TOP_10/A1
PCI/6.5.2

So if we want to really get rid of all remote command execution alerts, we could do the following replacing the SecRuleRemoveById from above:

# ModSec Rule Exclusion: Rules tagged with "attack-rce"
SecRuleRemoveByTag 'attack-rce'

Technically there an additional directive, SecRuleRemoveByMsg. However, the messages are not guaranteed to be stable between releases and they are not very consistent anyways. So you should not try to build exlcusion rules for the Core Rule Set via this directive.

So these are startup rule exclusions. Excluding a rule in this manner is simple and readable, but it is also a drastic step which we will not use in a production setup very often. Because, if our issues with the rule 932160 are limited to a single URI where people submit their login credentials, then we can limit the exclusion to this individual request. This is no longer a startup time rule exclusion, but a runtime exclusion which is being applied on certain conditions. Runtime exclusions leverage the SecRule directive combined with a special action executing the rule exclusion. This depends on the SecRule statement running before the rule in question is applied. That's why runtime rule exclusions have to be placed before the Core Rule Set include statement, where we also reserved a space for this type of exclusion rule:

# === ModSec Core Rules: Runtime Exclusion Rules (ids: 10000-49999)

# ModSec Exclusion Rule: 932160 Remote Command Execution: Unix Shell Code Found
SecRule REQUEST_FILENAME "@streq /login/Login.do" \
    "phase:1,nolog,pass,id:15000,ctl:ruleRemoveById=932160"

Now this is harder to read. Watch out for the ctl statement: ctl:ruleRemoveById=932160. This is the control action, which is used for runtime changes of the configuration of the ModSecurity rule engine. We use ruleRemoveById as the control statement and apply it to rule ID 932160. This block is placed within a standard SecRule directive. This allows us to use the complete power of SecRule to exclude rule 932160 in very specific situations. Here we exclude it based on the path of the request, but we could apply it depending on the agent's IP address - or a combination of the two in a chained rule statement.

As with the startup rule exclusions, we are not limited to an exclusion by rule ID. Exclusions by tag will work just as well (ctl:ruleRemoveByTag).

Startup time rule exclusions and runtime rule exclusions have the same effect, but internally, they are really different. With the runtime exclusions, you gain granular control at the cost of performance, as the exclusion is being evaluated for every single request. Startup time exclusions are performing faster and they are easier to read and write.

Step 9: Handling false positives: Disabling individual rules for specific parameters

Next we look at excluding an individual parameter from being evaluated by a specific rule. So unlike our examples removing the rule 932160 completely, we are now targeting rules examining the ARGS group of variables.

I have stated before that removing the complete rule because of the false alert was wrong. It is much better to only remove the rule for the password parameter, because it serves a very important purpose with many other parameters. Ideally, we want to exclude the parameter password from being examined by this rule. Here is the startup time rule exclusion performing this task:

# ModSec Exclusion Rule: 932160 Remote Command Execution: Unix Shell Code Found
SecRuleUpdateTargetById 932160 !ARGS:password

This directive adds "not ARGS:password" to the list of parameters to be examined by rule 932160. This effectively excludes the parameter from the evaluation. This directive also accepts rule ranges as parameters. Of course, this directive also exists in a variant where we select the rule via its tag:

# ModSec Exclusion Rule: Rules tagged with "attack-rce"
SecRuleUpdateTargetByTag "attack-rce" !ARGS:password

A password parameter is generally only used on the login request and where you update the password. So we can work with the SecRuleUpdateTargetById directive in practice and all occurrences of said parameter are exempt from examination by rule 932160. But let me stress, that this directive is server-wide. If you have multiple services with multiple NGINX virtual hosts each running a different application, then SecRuleUpdateTargetById and SecRuleUpdateTargetByTag will disable the said rule or rules respectively for all occurrences of the password parameter on the whole server.

So let's assume you want to exclude password only under certain conditions. For example the rule should still be active when a scanner is submitting the request. One fairly good way to detect scanners is by looking at the Referer request header. So the idea is to check the correct header and then exclude the parameter from examination by 932160. This runtime rule exclusion works with a control action, similar to the ones we have seen before:

SecRule REQUEST_HEADERS:Referer "@streq http://localhost/login/displayLogin.do" \
    "phase:1,nolog,pass,id:15000,ctl:ruleRemoveTargetById=932160;ARGS:password"

The format of the control action is really difficult to grasp now: In addition to the rule ID, we add a semicolon and then the password parameter as part of the ARGS group of variables. In ModSecurity, this is called the ARGS collection with the colon as separator. Try to memorize this!

In professional use, this is likely the exclusion rule construct that is used the most (not with the Referer header, though, but with the REQUEST_FILENAME variable). This exclusion construct is very granular on the parameter level and it can be constructed to have only minimal impact on the requests thanks to the power of SecRule. If you would rather go with a tag than with an ID, here is your example:

SecRule REQUEST_HEADERS:Referer "@streq http://localhost/login/displayLogin.do" \
    "phase:1,nolog,pass,id:15000,ctl:ruleRemoveTargetByTag=attack-rce;ARGS:password"

This section was very important. Therefore, to summarize once again: We define a rule to suppress another rule. We use a pattern for this which lets us define a path as a condition. This enables us to disable rules for individual parts of an application but only in places where false alarms occur. And at the same time, it prevents us from disabling rules on the entire server.

With this, we have seen all basic methods to handle false positives via rule exclusions. You now use the patterns for excusion rules described above to work through the various false positives.

Step 10 (Goodie): Summary of the ways of combating false positives

It is possibly best to summarize the tuning directives in a graphic. So here is a cheatsheet for your use!

Rule Exclusion CheatSheet

References

License / Copying / Further use

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.