Skip to content

fix: clean matched vars after chained and non-chained rule #3418

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

Open
wants to merge 2 commits into
base: v3/master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/rule_with_operator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,9 @@ bool RuleWithOperator::evaluate(Transaction *trans,

/* last rule in the chain. */
performLogging(trans, ruleMessage, true, true);
if (m_ruleId > 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this check? If I understand correctly, m_ruleId == 0 would be an invalid rule (caught exception). Would we even get here then? As far as I can tell, nothing bad would happen if m_ruleId == 0, since cleanMatchedVars() operates on the transaction only.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, m_ruleId == 0 would be an invalid rule

Yes, except if it's a chained rule. Generally, every chained rule (rules?) has (have?) only one unique id. In libmodsecurity3, despite you set up the id action at the first rule (in a chained rule), the last rule will own that id. Therefore this condition (m_ruleId > 0) tells us this is the end of a rule, no matter that's chained or not, we should clean the MATCHED_* variables.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. From the code (and the comment) it seems like the condition is redundant though, since end_exec: will always be evaluated for the last rule. So I still don't see why the extra condition is necessary.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we need to clear MATCHED_* variables only if the rule is a standalone rule (not chained), or it's the last part of a chained rule.

This means if the rule is part if a chained rule, but not the last, then we don't clean these group of variables.

If you see the diff, we can say this solves the issue.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand, and that's exactly what I'm saying... this will only be true when all chained rules have been processed. This will only be true when the rule is not chained. In all cases,end_exec will only be evaluated when the last rule has been reached, whether it's the last in a chain or a single rule. Hence: end_exec is equivalent to m_ruleId > 0. Unless I still don't get it...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me approach it from a different direction: if that condition were not there, the MATCHED_* variables would be deleted after each rule was evaulated. Not just after standalone rules, but in case of every part in chained rules. For eg. take a look at the initial rule in original issue:

SecRule ARGS pattern "chain,deny,id:28"
  SecRule MATCHED_VARS_NAMES "@eq ARGS:param"

and the request:

GET /?foo=1&bar=2&baz=pattern

If the condition were not there, then the first part of chained rule matches, MATCHED_* variables are set, but the engine clears them immediately after the end of processing, therefore the second part of the chained rule wont match, because MATCHED_VARS_NAMES is empty.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 cents:

  • I think when there are different opinions what the code is doing, having a test to confirm which interpretation is correct would be always nice ;-)
  • How about clearing matched vars not at the end, but as the first action when starting to evaluate a rule (or chain of rules)? Should be semantically the same and we need only a single place to clean matched vars.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I worked on this fix I made several tests, this is why I could make this patch 😃.

Btw I try to figure out how can I solve the issue with your idea (clear vars before the evaluation). (As I remember I tried to go on this way, but I rejected for some reason... Let me check again.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I think I know why did I reject that solution.

Let's approach the issue from the transaction's direction. Transaction object calls the evaluate() function here, for eg. (there are several other call, in each phases).

The evaluate() is part of transaction's m_rules member, which is a RuleSet type, see here. m_rules->evaluate() has two arguments: phase and transaction.

It grabs the rules in phase that called, see here, and iterates through them. There is an auto cast here, but the rule which created here can be a RuleWithActions or RuleWithOperator.

But only the RuleWithOperator has cleanMatchedVars.

I didn't want to touch this, therefore I tried to find a solution at the end of the rule processing. But we can find a different solution, eg. we can move the cleanMatchedVars() to another class. Then I think we can make clean method easier.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation @airween. I still don't understand why the condition is necessary, but your tests prove that the code works, so that's good enough for me. I suggest you add a comment before the condition that explains what the condition is for. Then I'll be happy.

cleanMatchedVars(trans);
}
return true;
}

Expand Down
127 changes: 125 additions & 2 deletions test/test-cases/regression/variable-MATCHED_VAR.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"enabled":1,
"version_min":300000,
"title":"Testing Variables :: MATCHED_VAR (1/2)",
"title":"Testing Variables :: MATCHED_VAR (1/5)",
"client":{
"ip":"200.249.12.31",
"port":123
Expand Down Expand Up @@ -42,7 +42,7 @@
{
"enabled":1,
"version_min":300000,
"title":"Testing Variables :: MATCHED_VAR (2/2)",
"title":"Testing Variables :: MATCHED_VAR (2/5)",
"client":{
"ip":"200.249.12.31",
"port":123
Expand Down Expand Up @@ -81,6 +81,129 @@
"SecRule MATCHED_VAR \"@contains other_value\" \"id:29,pass\"",
"SecRule MATCHED_VAR \"@contains other_value\" \"id:30,pass\""
]
},
{
"enabled":1,
"version_min":300000,
"title":"Testing Variables :: MATCHED_VAR (3/5)",
"client":{
"ip":"200.249.12.31",
"port":123
},
"server":{
"ip":"200.249.12.31",
"port":80
},
"request":{
"headers":{
"Host":"localhost",
"User-Agent":"curl/7.38.0",
"Accept":"*/*"
},
"uri":"/?foo=1&bar=2&baz=2",
"method":"GET"
},
"response":{
"headers":{
"Date":"Mon, 13 Jul 2015 20:02:41 GMT",
"Last-Modified":"Sun, 26 Oct 2014 22:33:37 GMT",
"Content-Type":"text/html"
},
"body":[
"no need."
]
},
"expected":{
"http_code": 200
},
"rules":[
"SecRuleEngine On",
"SecRule ARGS \"@rx 1\" \"id:1,phase:1,pass\"",
"SecRule ARGS \"@rx 2\" \"id:2,phase:1,pass\"",
"SecRule MATCHED_VAR \"@eq 1\" \"id:3,phase:1,deny,status:403\""
]
},
{
"enabled":1,
"version_min":300000,
"title":"Testing Variables :: MATCHED_VAR (4/5)",
"client":{
"ip":"200.249.12.31",
"port":123
},
"server":{
"ip":"200.249.12.31",
"port":80
},
"request":{
"headers":{
"Host":"localhost",
"User-Agent":"curl/7.38.0",
"Accept":"*/*"
},
"uri":"/?foo=1&bar=2&baz=2",
"method":"GET"
},
"response":{
"headers":{
"Date":"Mon, 13 Jul 2015 20:02:41 GMT",
"Last-Modified":"Sun, 26 Oct 2014 22:33:37 GMT",
"Content-Type":"text/html"
},
"body":[
"no need."
]
},
"expected":{
"http_code": 200
},
"rules":[
"SecRuleEngine On",
"SecRule ARGS \"@rx 1\" \"id:1,phase:1,pass\"",
"SecRule ARGS \"@rx 2\" \"id:2,phase:1,pass\"",
"SecRule MATCHED_VAR \"@eq 2\" \"id:3,phase:1,deny,status:403\""
]
},
{
"enabled":1,
"version_min":300000,
"title":"Testing Variables :: MATCHED_VAR (5/5)",
"client":{
"ip":"200.249.12.31",
"port":123
},
"server":{
"ip":"200.249.12.31",
"port":80
},
"request":{
"headers":{
"Host":"localhost",
"User-Agent":"curl/7.38.0",
"Accept":"*/*"
},
"uri":"/?foo=1&bar=2&baz=2",
"method":"GET"
},
"response":{
"headers":{
"Date":"Mon, 13 Jul 2015 20:02:41 GMT",
"Last-Modified":"Sun, 26 Oct 2014 22:33:37 GMT",
"Content-Type":"text/html"
},
"body":[
"no need."
]
},
"expected":{
"http_code": 403
},
"rules":[
"SecRuleEngine On",
"SecRule ARGS \"@rx 1\" \"id:1,phase:1,pass\"",
"SecRule ARGS \"@rx 2\" \"id:2,phase:1,deny,status:403,chain\"",
"SecRule MATCHED_VAR \"@eq 2\""
]
}
]

168 changes: 166 additions & 2 deletions test/test-cases/regression/variable-MATCHED_VARS.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"enabled":1,
"version_min":300000,
"title":"Testing Variables :: MATCHED_VARS (1/2)",
"title":"Testing Variables :: MATCHED_VARS (1/6)",
"client":{
"ip":"200.249.12.31",
"port":123
Expand Down Expand Up @@ -43,7 +43,7 @@
{
"enabled":1,
"version_min":300000,
"title":"Testing Variables :: MATCHED_VARS (2/2)",
"title":"Testing Variables :: MATCHED_VARS (2/6)",
"client":{
"ip":"200.249.12.31",
"port":123
Expand Down Expand Up @@ -81,6 +81,170 @@
"SecRule MATCHED_VARS \"@contains asdf\" \"\"",
"SecRule MATCHED_VARS \"@contains value\" \"id:29\""
]
},
{
"enabled":1,
"version_min":300000,
"title":"Testing Variables :: MATCHED_VARS (3/6)",
"client":{
"ip":"200.249.12.31",
"port":123
},
"server":{
"ip":"200.249.12.31",
"port":80
},
"request":{
"headers":{
"Host":"localhost",
"User-Agent":"curl/7.38.0",
"Accept":"*/*"
},
"uri":"/?foo=1&bar=2&baz=2",
"method":"GET"
},
"response":{
"headers":{
"Date":"Mon, 13 Jul 2015 20:02:41 GMT",
"Last-Modified":"Sun, 26 Oct 2014 22:33:37 GMT",
"Content-Type":"text/html"
},
"body":[
"no need."
]
},
"expected":{
"http_code": 200
},
"rules":[
"SecRuleEngine On",
"SecRule ARGS \"@rx 1\" \"id:1,phase:1,pass\"",
"SecRule ARGS \"@rx 2\" \"id:2,phase:1,pass\"",
"SecRule MATCHED_VARS \"@contains 1\" \"id:3,phase:1,deny,status:403\""
]
},
{
"enabled":1,
"version_min":300000,
"title":"Testing Variables :: MATCHED_VARS (4/6)",
"client":{
"ip":"200.249.12.31",
"port":123
},
"server":{
"ip":"200.249.12.31",
"port":80
},
"request":{
"headers":{
"Host":"localhost",
"User-Agent":"curl/7.38.0",
"Accept":"*/*"
},
"uri":"/?foo=1&bar=2&baz=2",
"method":"GET"
},
"response":{
"headers":{
"Date":"Mon, 13 Jul 2015 20:02:41 GMT",
"Last-Modified":"Sun, 26 Oct 2014 22:33:37 GMT",
"Content-Type":"text/html"
},
"body":[
"no need."
]
},
"expected":{
"http_code": 200
},
"rules":[
"SecRuleEngine On",
"SecRule ARGS \"@rx 1\" \"id:1,phase:1,pass\"",
"SecRule ARGS \"@rx 2\" \"id:2,phase:1,pass\"",
"SecRule MATCHED_VARS \"@contains 2\" \"id:3,phase:1,deny,status:403\""
]
},
{
"enabled":1,
"version_min":300000,
"title":"Testing Variables :: MATCHED_VARS (5/6)",
"client":{
"ip":"200.249.12.31",
"port":123
},
"server":{
"ip":"200.249.12.31",
"port":80
},
"request":{
"headers":{
"Host":"localhost",
"User-Agent":"curl/7.38.0",
"Accept":"*/*"
},
"uri":"/?foo=1&bar=2&baz=2",
"method":"GET"
},
"response":{
"headers":{
"Date":"Mon, 13 Jul 2015 20:02:41 GMT",
"Last-Modified":"Sun, 26 Oct 2014 22:33:37 GMT",
"Content-Type":"text/html"
},
"body":[
"no need."
]
},
"expected":{
"http_code": 200
},
"rules":[
"SecRuleEngine On",
"SecRule ARGS \"@rx 1\" \"id:1,phase:1,pass\"",
"SecRule ARGS \"@rx 2\" \"id:2,phase:1,pass\"",
"SecRule MATCHED_VARS \"@within 1 2\" \"id:3,phase:1,deny,status:403\""
]
},
{
"enabled":1,
"version_min":300000,
"title":"Testing Variables :: MATCHED_VARS (6/6)",
"client":{
"ip":"200.249.12.31",
"port":123
},
"server":{
"ip":"200.249.12.31",
"port":80
},
"request":{
"headers":{
"Host":"localhost",
"User-Agent":"curl/7.38.0",
"Accept":"*/*"
},
"uri":"/?foo=1&bar=2&baz=2",
"method":"GET"
},
"response":{
"headers":{
"Date":"Mon, 13 Jul 2015 20:02:41 GMT",
"Last-Modified":"Sun, 26 Oct 2014 22:33:37 GMT",
"Content-Type":"text/html"
},
"body":[
"no need."
]
},
"expected":{
"http_code": 403
},
"rules":[
"SecRuleEngine On",
"SecRule ARGS \"@rx 1\" \"id:1,phase:1,pass\"",
"SecRule ARGS \"@rx 2\" \"id:2,phase:1,deny,status:403,chain\"",
"SecRule MATCHED_VARS \"@eq 2\""
]
}
]

Loading
Loading