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
Multiphase: chains further support, ARGS split, CRS like tests #719
Conversation
@M4tteoP Do you mind rebasing? |
testing/engine/allow.go
Outdated
}, | ||
}, | ||
}, | ||
// TODO(MultiPhase)[MovingIntoAllowedPhase]: see rules 45 and 46. Rule 45 allows all the request phases (1 and 2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some ideas.
We should keep in mind that in the end, our main target is CRS so optimizing the behavior for CRS while still correctly handling rules is OK.
Bluntest hammer: disable multiphase evaluation if any rule contains an allow action. AFAICT, CRS does not contain any allow actions, which makes senes, and is why FTW is working I guess.
It's too blunt I guess, the main reason for allow would be for users to be able to override CRS behavior for their unique needs.
So if we can come up with a user experience that provides this and document it, and return errors otherwise, then it could be OK.
For example, implement the following type of rules
Multiphase is enabled if allow actions are only in phase 1
Multiphase is enabled if allow actions are only in the lowest-priority rules of a given phase (we could change our inferrence to only allow upgrading phase 2 to phase 1, phase 4 to phase 3, rather than having e.g. phase 3 upgrade to phase 1, that doesn't seem to have much use case)
...any other condition that seems to make sense
We should be able to detect cases where these don't hold and return a configuration error with guidance on how configs can be tweaked to define an allow rule that works with multiphase.
This may not allow bypassing every single situation - I think the above would not allow a request body to contain a log4shell string, and to also contain the string IAmSafe and be allowed by a rule as log4shell would always win. An obvious solution to this that respects ordering between allow/deny doesn't come to mind - but I don't know if this is useful enough to worry, as long as we return a configuration error rather than ignoring, it should be OK. I would suspect the vast majority of allow to be in phase 1, based on headers/cookies? Don't know but basically if the cases we can't enable multiphase for are corner cases, it should be OK, we just have to make sure we tell the user why when we can't.
Does it seem possible?
01d5e4d
to
be2e629
Compare
… rule tests compatible
1876033
to
daeff16
Compare
…sonings, excludes allow and skip tests w/multiphase
Codecov ReportPatch coverage:
Additional details and impacted files@@ Coverage Diff @@
## v3/dev #719 +/- ##
==========================================
+ Coverage 81.96% 82.46% +0.50%
==========================================
Files 153 154 +1
Lines 8256 8463 +207
==========================================
+ Hits 6767 6979 +212
+ Misses 1272 1263 -9
- Partials 217 221 +4
Flags with carried forward coverage won't be shown. Click here to find out more.
☔ View full report in Codecov by Sentry. |
This PR now provides the following:
Fixes/Changes:
I moved all the reasonings out of the PR into this gist. As we agreed, I meanly focused on Multiphase evaluation with CRS. Please take a deep look if it seems right 🙏 @anuraaga @jcchavezs |
Is there anything known after this PR that we expect could be a problem when not using CRS? |
I think before merging this we should test it in proxy wasm to see if we can fix the bypass. |
internal/corazawaf/rule.go
Outdated
// If we reached this point, it means that the parent rule already reached its min phase. | ||
if multiphaseEvaluation && r.ParentID_ == 0 && r.HasChain && r.chainMinPhase == types.PhaseUnknown { | ||
for c := r.Chain; c != nil; c = c.Chain { | ||
singleChainedRuleMinPhase := types.PhaseUnknown |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While doing some manual tests with Envoy I found that we were trying to evaluate Chained rules too much, in certain circumstances bypassing also paranoia levels.
I refactored a bit the way we compute chainMinPhase
. Now the chainMinPhase is the latest minPhase across all the chained rules.
For example:
SecRule REQUEST_URI "/chain_phase2" "id:10, phase:2, t:none, log, setvar:'tx.set1=1', chain"
SecRule REQUEST_URI "/chain_phase2" "setvar:'tx.set2=2', chain"
SecRule REQUEST_BODY "chain_phase2" "setvar:'tx.set3=3'"
Here I think that REQUEST_BODY is the "bottle neck" of this chain, therefore it is the chainMinPhase. Without being able to evaluate it, the chain will never match.
internal/corazawaf/rule.go
Outdated
// 00000010 & 00000100 | ||
// If any of the them is true, it returns true and stops iterating | ||
func (p *inferredPhases) hasOrMinor(phase types.RulePhase) bool { | ||
for i := 1; i <= int(phase); i++ { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first idea was relying on a mask (looking at the example like 00000111), but I found that the code was becoming more cryptic, with supported variables. I went for the small loop.
Happy if any better implementation can be done without too many byzantine bit tricks
12c1cc6
to
ad8231f
Compare
if !multiphaseEvaluation || | ||
(!r.HasChain && !r.inferredPhases.has(phase)) || | ||
(r.HasChain && phase < r.chainMinPhase) || | ||
(r.HasChain && !r.inferredPhases.hasOrMinor(phase) && !r.withPhaseUnknownVariable) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here I added yet another control to reduce chained rules evaluated at impossible times. hasOrMinor is an extended way to look at the inferredPhases of the parent rule, returning true not only if the specific phase is inferred, but also if any other previous phase was already an inferred phase.
For example:
SecRule REQUEST_URI "/chain_phase2" "id:10, phase:2, t:none, log, setvar:'tx.set1=1', chain"
SecRule REQUEST_BODY "chain_phase2" "setvar:'tx.set2=2', chain"
SecRule REQUEST_BODY "chain_phase2" "setvar:'tx.set3=3'"
Even if the parent rule has a single variable at phase:1, during phase 2 these rule has to be considered ready to be evaluated again participating in the match of the whole chain.
Vice versa, if the parent rule has variables that are not yet ready, there is no point in evaluating the chain because it will never match (this is the control enforced here).
The only exception I see, and I don't like much making these checks even more complex, is when a PhaseUnknown variable is the only variable in place. (Eg see 920250
). This is the reason why I had to add the withPhaseUnknownVariable
boolean.
I ran some manual requests based on https://github.com/M4tteoP/coraza-proxy-wasm/tree/multiphase. Results seem pretty good: both simple and chained rules are getting anticipated, also thanks to the ARGS splitting. Some requests outputs:
I also updated the gist with known problems and reasonings. The main concern that I currently see is about Multiple evaluations of the same variables on more than one phase. Chained rules anticipated and triggered in a previous phase, then will match again in the next phase. Please look at the gist for details and a related test. @anuraaga @jcchavezs |
I would merge this as it is. However one things which drives me crazy is that this package (before changes) is already poorly tested. I tried to add some more coverage before merging this but did not have enough time. If I none (in case someone is in) adds more coverage to this package pre this PR let's just merge it tomorrow. |
d7a3403
to
a0dd4ff
Compare
Could you please give it another review @anuraaga? |
PR built on top of #692.
This PR wants to create a suitable ground for further reasoning about Multiphase evaluation and how to implement it by looking at some of the problems found at once:
@anuraaga @jcchavezs