Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Incorrect log message from modsec v3 for CRS rule 920450 #2423

Closed
michaelgranzow-avi opened this issue Oct 16, 2020 · 10 comments
Closed

Incorrect log message from modsec v3 for CRS rule 920450 #2423

michaelgranzow-avi opened this issue Oct 16, 2020 · 10 comments
Assignees
Labels
3.x Related to ModSecurity version 3.x
Projects

Comments

@michaelgranzow-avi
Copy link
Contributor

michaelgranzow-avi commented Oct 16, 2020

Describe the bug

In chained rules, if actions 'msg' or 'logdata' contain variables like %{MATCHED_VAR}, and are used in a rule that isn't the last in the chain, the wrong MATCHED_VAR can end up in the logs.

Logs and dumps

The following regression test reproduces the bug:

[
  {
    "enabled":1,
    "version_min":300000,
    "title":"Test match variable",
    "expected":{
        "http_code": 437,
        "error_log": "msg \"Illegal header \\[restricted\\]\""
    },
    "client":{
      "ip":"200.249.12.31",
      "port":123
    },
    "request":{
      "headers":{
        "Host":"localhost",
        "Restricted":"attack",
        "Other":"Value"
      },
      "uri":"/",
      "method":"GET"
    },
    "server":{
      "ip":"200.249.12.31",
      "port":80
    },
    "rules":[
      "SecRuleEngine On",
      "SecRule REQUEST_HEADERS_NAMES \"^.*$\" \"phase:2,setvar:'tx.header_name_%{TX.0}=/%{TX.0}/',deny,t:lowercase,capture,id:500065,msg:'Illegal header [%{MATCHED_VAR}]',logdata:'Restricted header detected: %{MATCHED_VAR}',chain\"",
      "SecRule TX:/^header_name_/ \"@within /name1/restricted/name3/\" \"status:437\""
    ]
  }
]

Full logs from regression test:

Debug log:
[1602861515] [] [4] Initializing transaction
[1602861515] [] [4] Transaction context created.
[1602861515] [] [4] Starting phase CONNECTION. (SecRules 0)
[1602861515] [] [9] This phase consists of 0 rule(s).
[1602861515] [] [4] Starting phase URI. (SecRules 0 + 1/2)
[1602861515] [/] [4] Starting phase REQUEST_HEADERS.  (SecRules 1)
[1602861515] [/] [9] This phase consists of 0 rule(s).
[1602861515] [/] [9] Appending request body: 0 bytes. Limit set to: 0.000000
[1602861515] [/] [4] Starting phase REQUEST_BODY. (SecRules 2)
[1602861515] [/] [9] This phase consists of 1 rule(s).
[1602861515] [/] [4] (Rule: 500065) Executing operator "Rx" with param "^.*$" against REQUEST_HEADERS_NAMES.
[1602861515] [/] [9]  T (0) t:lowercase: "host"
[1602861515] [/] [9] Target value: "host" (Variable: REQUEST_HEADERS_NAMES:Host)
[1602861515] [/] [7] Added regex subexpression TX.0: host
[1602861515] [/] [9] Matched vars updated.
[1602861515] [/] [4] Running [independent] (non-disruptive) action: setvar
[1602861515] [/] [8] Saving variable: TX:header_name_host with value: /host/
[1602861515] [/] [9] Saving msg: Illegal header [host]
[1602861515] [/] [9]  T (0) t:lowercase: "restricted"
[1602861515] [/] [9] Target value: "restricted" (Variable: REQUEST_HEADERS_NAMES:Restricted)
[1602861515] [/] [7] Added regex subexpression TX.0: restricted
[1602861515] [/] [9] Matched vars updated.
[1602861515] [/] [4] Running [independent] (non-disruptive) action: setvar
[1602861515] [/] [8] Saving variable: TX:header_name_restricted with value: /restricted/
[1602861515] [/] [9] Saving msg: Illegal header [restricted]
[1602861515] [/] [9]  T (0) t:lowercase: "other"
[1602861515] [/] [9] Target value: "other" (Variable: REQUEST_HEADERS_NAMES:Other)
[1602861515] [/] [7] Added regex subexpression TX.0: other
[1602861515] [/] [9] Matched vars updated.
[1602861515] [/] [4] Running [independent] (non-disruptive) action: setvar
[1602861515] [/] [8] Saving variable: TX:header_name_other with value: /other/
[1602861515] [/] [9] Saving msg: Illegal header [other]
[1602861515] [/] [4] Rule returned 1.
[1602861515] [/] [4] Executing chained rule.
[1602861515] [/] [4] (Rule: 0) Executing operator "Within" with param "/name1/restricted/name3/" against TX:regex(^header_name_).
[1602861515] [/] [9] Target value: "/host/" (Variable: TX:header_name_host)
[1602861515] [/] [9] Target value: "/restricted/" (Variable: TX:header_name_restricted)
[1602861515] [/] [9] Matched vars updated.
[1602861515] [/] [9] Target value: "/other/" (Variable: TX:header_name_other)
[1602861515] [/] [4] Rule returned 1.
[1602861515] [/] [9] Running action: status
[1602861515] [/] [4] Running (disruptive)     action: deny.
[1602861515] [/] [8] Running action deny
[1602861515] [/] [8] Skipping this phase as this request was already intercepted.
[1602861515] [/] [4] Starting phase RESPONSE_HEADERS. (SecRules 3)
[1602861515] [/] [9] This phase consists of 0 rule(s).
[1602861515] [/] [9] Appending response body: 0 bytes. Limit set to: 0.000000
[1602861515] [/] [4] Starting phase RESPONSE_BODY. (SecRules 4)
[1602861515] [/] [4] Response body is disabled, returning... 2
[1602861515] [/] [4] Starting phase LOGGING. (SecRules 5)
[1602861515] [/] [9] This phase consists of 0 rule(s).
[1602861515] [/] [8] Checking if this request is suitable to be saved as an audit log.
[1602861515] [/] [8] Checking if this request is relevant to be part of the audit logs.
[1602861515] [/] [5] Audit log engine was not set.
[1602861515] [/] [8] Request was relevant to be saved. Parts: 4430

Error log:
[client 200.249.12.31] ModSecurity: Access denied with code 437 (phase 2). Matched "Operator `Within' with parameter `/name1/restricted/name3/' against variable `TX:header_name_restricted' (Value: `/restricted/' ) [file "r3.json"] [line "2"]\
 [id "500065"] [rev ""] [msg "Illegal header [other]"] [data "Restricted header detected: other"] [severity "0"] [ver ""] [maturity "0"] [accuracy "0"] [hostname "200.249.12.31"] [uri "/"] [unique_id "1602861515"] [ref "o0,4v12,4t:lowercaseo\
0,10v28,10t:lowercaseo0,5v47,5t:lowercaseo6,12"]

Expected behavior

msg shows "Illegal header [restricted]" . Observed: "Illegal header [other]"

Server (please complete the following information):

  • current ModSecurity version v3/master (commit 377fb72)

Rule Set (please complete the following information):
Issue was spotted in CRS rule 920450, but the above rule is the minimal scenario to reproduce it.

Additional context

Tests have shown that modsec v2 shows the expected behavior.

@zimmerle zimmerle self-assigned this Oct 16, 2020
@zimmerle zimmerle added the 3.x Related to ModSecurity version 3.x label Oct 16, 2020
@zimmerle
Copy link
Contributor

Interesting observation.

Thank you for the regression @michaelgranzow-avi that speed up things.

@zimmerle zimmerle added this to Backlog in v3.1.0 via automation Oct 16, 2020
@michaelgranzow-avi
Copy link
Contributor Author

@zimmerle I've improved the test, so it actually checks for the error log.

@michaelgranzow-avi
Copy link
Contributor Author

michaelgranzow-avi commented Oct 19, 2020

One more observation: the behaviour in v2 is not fundamentally different. That is, the order of loops over rules, variables, transformations etc is similar. What is different is that 'meta actions' like id, msg etc can only be attached to the first rule in a chain. And those actions are executed after all rules have fired in the chain. Therefore, the 'msg' action in the test above is executed when the second rule fires. At that stage, %{MATCHED_VAR} is '/restricted/', the value of the variable 'TX:header_name_restricted, and hence the log contains the following message:

msg "Illegal header [/restricted/]"

To emphasize: also in v2, we don't see the variable that was the root cause of the rejection by modsec (this would be REQUEST_HEADERS:Restricted), but the variable that was constructed in the first and used in the second rule.

The main difference seems to be that in v3 those 'meta actions' don't get executed again at the end, when the second rule fires. If I place the 'msg' action with the second rule in the chain (which isn't allowed in v2), I get this log message, the same as v2:

msg "Illegal header [/restricted/]"

@michaelgranzow-avi
Copy link
Contributor Author

michaelgranzow-avi commented Oct 20, 2020

The following patch fixes the issue for me (and doesn't break any of the existing regression tests):

commit 2747a5c219f7e4fa06357ac6ad5cde6902ae62dc
Author: Michael Granzow <mgranzow@vmware.com>
Date:   Tue Oct 20 11:18:23 2020 +0000

    Issue 2423: Run actions 'msg', 'logdata' and 'severity' only at the end of a chain

diff --git a/src/rule_with_actions.cc b/src/rule_with_actions.cc
index a2d7306..5e7e113 100644
--- a/src/rule_with_actions.cc
+++ b/src/rule_with_actions.cc
@@ -215,17 +215,6 @@ void RuleWithActions::executeActionsIndependentOfChainedRuleResult(Transaction *
         }
     }

-    if (m_severity) {
-        m_severity->evaluate(this, trans, ruleMessage);
-    }
-
-    if (m_logData) {
-        m_logData->evaluate(this, trans, ruleMessage);
-    }
-
-    if (m_msg) {
-        m_msg->evaluate(this, trans, ruleMessage);
-    }
 }


@@ -257,6 +246,17 @@ void RuleWithActions::executeActionsAfterFullMatch(Transaction *trans,
         executeAction(trans, containsBlock, ruleMessage, a, false);
         disruptiveAlreadyExecuted = true;
     }
+    if (m_severity) {
+        m_severity->evaluate(this, trans, ruleMessage);
+    }
+
+    if (m_logData) {
+        m_logData->evaluate(this, trans, ruleMessage);
+    }
+
+    if (m_msg) {
+        m_msg->evaluate(this, trans, ruleMessage);
+    }
     for (Action *a : this->m_actionsRuntimePos) {
         if (!a->isDisruptive()
                 && !(disruptiveAlreadyExecuted
diff --git a/test/test-cases/regression/msg_in_chain.json b/test/test-cases/regression/msg_in_chain.json
new file mode 100644
index 0000000..9ea7803
--- /dev/null
+++ b/test/test-cases/regression/msg_in_chain.json
@@ -0,0 +1,33 @@
+[
+  {
+    "enabled":1,
+    "version_min":300000,
+    "title":"Test match variable",
+    "expected":{
+        "http_code": 437,
+       "error_log": "msg \"Illegal header \\[/restricted/\\]\""
+    },
+    "client":{
+      "ip":"200.249.12.31",
+      "port":123
+    },
+    "request":{
+      "headers":{
+        "Host":"localhost",
+          "Restricted":"attack",
+         "Other": "Value"
+      },
+      "uri":"/",
+      "method":"GET"
+    },
+    "server":{
+      "ip":"200.249.12.31",
+      "port":80
+    },
+    "rules":[
+      "SecRuleEngine On",
+      "SecRule REQUEST_HEADERS_NAMES \"^.*$\" \"phase:2,setvar:'tx.header_name_%{TX.0}=/%{TX.0}/',deny,t:lowercase,capture,id:500065,msg:'Illegal header [%{MATCHED_VAR}]',logdata:'Restricted header detected: %{MATCHED_VAR}',chain\"",
+      "SecRule TX:/^header_name_/ \"@within /name1/restricted/name3/\" \"status:437\""
+    ]
+  }
+]

I would open a PR, but it seems I don't have the permissions.

@zimmerle
Copy link
Contributor

@michaelgranzow-avi You should have permission for the pull request. However I would like to highlight the fact that we are also working on 3.1-experimental. There, this particular piece of code may look a bit different. In particular, how the MATCHED_VAR is set.

As a side note, do you have the effort to help us testing v3.1-experimental?

@michaelgranzow-avi
Copy link
Contributor Author

@zimmerle : I've created PRs against master/v3 and 3.1-experimental - please review.

@zimmerle
Copy link
Contributor

All merged. Patch available on both: v3/master and 3.1-experimental. Thank you @michaelgranzow-avi

v3.1.0 automation moved this from Backlog to Done Oct 29, 2020
@martinhsv
Copy link
Contributor

@michaelgranzow-avi (and @zimmerle if available), I would appreciate comment/insight on this matter.

I have been revisiting this issue because the related pull request broke some functionality (see #2573 )

From what I have seen so far, it looks to me like the premise of this issue seems questionable. It is entirely possible that I have misunderstood something, so feel free to correct me.

Allow me to explain the behaviour prior to the implementation of the pull request:

The first of the two rules in the chain will run against every entry in REQUEST_HEADERS_NAMES. There are three of them and there are no guarantees about which one will be examined last but it can very well be 'Other'

The 'msg' action runs and correctly populates the content using 'other'.

In other words the correct MATCHED_VAR is in the log.

Now, it is true that in the example case the match in the second rule of the chain is the one that is more meaningful to anyone examining the logs. But just because that happens to be the case in this example does not mean that will be true in every case. It's quite possible to imagine chained rules where the match in the first rule is actually the more meaningful one, and in other cases it might be the match in the 2nd-of-3 rules that is the most meaningful. In such cases it is now no longer possible to directly use MATCHED_VAR in order to output the desired information. (It would, of course, still be possible to assign to an intermediate variable to work around this.)

So, could not the sample rule have achieved the desired output simply by including the msg action on the 2nd rule? The following seems to work perfectly well pre- #2435:

SecRule TX:/^header_name_/ "@within /name1/restricted/name3/" "status:437,msg:'Illegal header [%{MATCHED_VAR}]',logdata:'Restricted header detected: %{MATCHED_VAR}'"

To summarize my concerns (other than the fact that this broke the aforementioned multimatch use case):

  • there seems to be a more intuitive way to write the rule chain to get the desired results without having implemented the related pull request
  • we have made things less useable for those for whom the most meaningful match is a rule other than the last
  • I think this makes things more confusing. Given that 'msg' can be specified on any rule of a chain, it's pretty non-obvious that MATCHED_VAR should always reference matches from the last rule in the chain. Intuitively, one would think that MATCHED_VAR on rule 1 of a chain should refer to the match from rule 1; MATCHED_VAR on rule 2 of a chain should refer to the match from rule 2; etc.

@martinhsv
Copy link
Contributor

Oh, I suppose I can guess the reason: because v2 only supports msg on the first rule of the chain.

@michaelgranzow-avi
Copy link
Contributor Author

Issue-2423: Meta-actions like 'msg' should be applied at end of chain #2436

That's one aspect. The other is that the order in which we check each variable is unpredictable, so if all variables match the first rule (which in the example is not a 'real' rule, it just populates a new variable list), msg will be executed on every variable, and what then ends up in the log can be any variable in the first list. I think even in the case you have in mind, where you want to see the match of the first rule, you would want the match 'corresponding' to the variable that also hit the second rule. However, the two rules have no idea about each other's variables (and in the general case, there doesn't have to be such a connection between two chained rules).

I think this exposes a limit of the modsec engine that forces users to write pseudo-rules that really correspond to program flow, not execution of checks on the request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.x Related to ModSecurity version 3.x
Projects
v3.1.0
  
Done
Development

No branches or pull requests

3 participants