Skip to content

perf: drop redundant exclusion check in rule evaluation - #3692

Open
chweidling wants to merge 2 commits into
owasp-modsecurity:v3/masterfrom
chweidling:perf/regex-exclusion-search
Open

chweidling wants to merge 2 commits into
owasp-modsecurity:v3/masterfrom
chweidling:perf/regex-exclusion-search

Conversation

@chweidling

@chweidling chweidling commented Oct 15, 2026 •

Copy link
Copy Markdown

what

  • RuleWithOperator::evaluate() no longer checks exclusion.contains(v) a second time. At that point it is always false: the check right before it already skips excluded values.
  • Variables::contains() uses Regex::search() instead of searchAll(), as it only needs to know whether a regex exclusion matches.
  • No public header or ABI change.

why

Every value that is not excluded paid for the scan over all exclusions twice, including the regex searches of exclusions like !REQUEST_COOKIES:'/^_ga/'. An exclusion added by tag, e.g. OWASP_CRS, applies to every rule.

With CRS v4 (download-owasp-v4-rules.sh) plus these exclusions, this change increases performance of the benchmark test by about 6%:

SecRuleUpdateTargetByTag "OWASP_CRS" "!REQUEST_COOKIES:'/^_ga/'"
SecRuleUpdateTargetByTag "OWASP_CRS" "!ARGS:'/^utm_/'"
SecRuleUpdateTargetByTag "attack-sqli" "!REQUEST_COOKIES:'/^(?:PHPSESSID|JSESSIONID)$/'"
SecRuleUpdateTargetByTag "attack-xss" "!REQUEST_HEADERS:'/^(?:Referer|Cookie)$/'"

Without regex exclusions there is no difference.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Regex-backed variable detection now distinguishes matches by their position in a key: matches at the beginning are not counted, while matches later in the key are.
    • Variable exclusions are checked separately before tag-based removals. Tag-removal matching behavior is unchanged.
RuleWithOperator::evaluate() checked exclusion.contains(v) twice for
every value that is not excluded. The second check can never be true,
because the first one already skips excluded values, but it repeats
the scan over all exclusions, including their regex searches.

Variables::contains() now uses Regex::search() instead of searchAll(),
as it only needs to know whether the regex matches.

With CRS v4 and four regex exclusions via SecRuleUpdateTargetByTag,
this increases performance of the benchmark test by about 6%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8674954a-eb49-42f6-8f75-35ed495ff63a
Commits

Reviewing files that changed from the base of the PR and between ba79b67 and 95f6d4f.

Files selected for processing: 1
  • src/variables/variable.h

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Walkthrough

The changes adjust variable exclusion handling in rule evaluation and regex-backed variable matching. Rule evaluation separates exclusion checks from tag-based target removals. Regex-backed variable matching uses whether search returns a nonzero result.

Changes: Variable exclusion matching

Layer / File(s) Summary
Exclusion checks
src/rule_with_operator.cc, src/variables/variable.h
RuleWithOperator::evaluate checks variable exclusions separately from tag-based target removals. Variables::contains uses the result of search to check regex-backed variables.

Priority: Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Possibly related PRs

Suggested reviewers: airween

Merge Risk: Minimal · up to 95f6d

The PR appears mergeable; no concrete correctness, availability, or integration risk was found.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title is a short, specific conventional-commit style summary that matches the changeset. The diff removes a redundant exclusion.contains(v) check in RuleWithOperator::evaluate, which the PR object…
Docstring Coverage Passed Docstring coverage is inconclusive: files from earlier reviews still need to be rechecked within the check's analysis limits. Not blocking.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title is a short, specific conventional-commit style summary that matches the changeset. The diff removes a redundant exclusion.contains(v) check in RuleWithOperator::evaluate, which the PR objectives describe as made redundant by the preceding check, and changes Variables::contains() to use Regex::search() instead of searchAll(). The title names the primary change (dropping a redundant exclusion check for performance) and does not describe anything absent from the changeset. It omits the Regex::search() change, which the title does not need to cover.

Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@airween airween added the 3.x Related to ModSecurity version 3.x label Oct 15, 2026

@fzipi fzipi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @chweidling, nice catch.

I checked both hunks:

  • rule_with_operator.cc: exclusion and v don't change between the first if (which already continues when exclusion.contains(v) is true) and the second one. So the removed term was always false, on every exclusion path (by id, tag or msg, key or regex). No behavior change.
  • variable.h: Regex::search(const std::string&) returns 1/0 (match or no match), not an offset. It uses the same JIT and no-JIT fallback as searchAll(), so search(...) > 0 is equivalent to !searchAll(...).empty(), including for empty matches like /^/. To be clear for anyone reading the bot summary above: matches at position 0 are still counted. The existing "anchored at beginning of Subject" cases in config-update-target-by-tag.json cover that and pass. I also checked !ARGS:'/^utm_/', !ARGS:'/utm_/' (match in the middle of the key) and !ARGS:'/^/' locally.

Tiny optional nit: != 0, or just using the result as a bool, would read less like an offset comparison. That's probably what tripped up CodeRabbit.

I built this locally on macOS (PCRE2 with JIT, together with #3691, which touches disjoint files). The exclusion and ctl regression files pass: config-update-target-by-tag/-by-id, issue-2296, issue-2099, action-ctl_rule_remove_by_tag, action-ctl_rule_remove_target_by_id/by_tag. The full make check passes as well (5128 total, 5110 pass, 18 skip, 0 fail). I didn't re-run the benchmark, but the reasoning for the gain with regex exclusions makes sense.

LGTM.

search() returns 1 for a match and 0 otherwise, not an offset. > 0
read like a position check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chweidling

Copy link
Copy Markdown
Author

Thanks a lot for the thorough review, @fzipi, and for checking the edge cases with /^/ and matches in the middle of the key.

I applied the nit: the comparison is now != 0 (95f6d4f).

This branch has not been deployed

No deployments
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

3 participants