Skip to content

fix: align HIGHEST_SEVERITY with ModSecurity behavior#1569

Open
majiayu000 wants to merge 10 commits intocorazawaf:mainfrom
majiayu000:fix/issue-1567-highest-severity
Open

fix: align HIGHEST_SEVERITY with ModSecurity behavior#1569
majiayu000 wants to merge 10 commits intocorazawaf:mainfrom
majiayu000:fix/issue-1567-highest-severity

Conversation

@majiayu000
Copy link
Copy Markdown

@majiayu000 majiayu000 commented Mar 23, 2026

Fixes #1567

Thank you for contributing to Coraza WAF, your effort is greatly appreciated
Before submitting check if what you want to add to coraza list meets quality standards before sending pull request. Thanks!

Make sure that you've checked the boxes below before you submit PR:

Summary

HIGHEST_SEVERITY variable was not aligned with ModSecurity behavior in two ways:

  1. Wrong default (waf.go): Initialized to "0" instead of "255". In ModSecurity, 255 is the sentinel value meaning "no severity set yet".

  2. Inverted comparison (transaction.go): Used > but severity is inverted (lower number = higher severity), so it should be <. Additionally, ParseRuleSeverity can't parse "255", which returned 0 on error and masked the sentinel.

Changes

  • internal/corazawaf/waf.go: Change default from "0" to "255"
  • internal/corazawaf/transaction.go: Fix comparison to use strconv.Atoi + < instead of ParseRuleSeverity + >
  • testing/engine/rulemetadata.go: Add test profiles for default value (255) and multiple severity tracking

Edge case note

Rules without an explicit severity action have Severity_=0 (Go zero value = RuleSeverityEmergency), which is indistinguishable from an explicitly set Emergency severity. In ModSecurity, rule default severity is -1 (not set) and doesn't affect HIGHEST_SEVERITY. A sentinel value or HasSeverity flag on RuleMetadata could address this, but that's a larger change — happy to discuss the best approach.

Thanks for your contribution ❤️

Summary by CodeRabbit

  • Bug Fixes

    • Fixed highest-severity tracking: HIGHEST_SEVERITY now defaults to 255 and is only updated when a rule explicitly defines a severity, ensuring it reflects the lowest numeric severity among fired rules.
  • Tests

    • Added test profiles validating default HIGHEST_SEVERITY, protection against missing-severity poisoning, and correct selection when multiple severities fire.

Default HIGHEST_SEVERITY to 255 (no severity set) instead of 0
(emergency). Fix comparison to use < instead of > since lower
numbers represent higher severity. Use strconv.Atoi instead of
ParseRuleSeverity which cannot parse the sentinel value 255.

Fixes corazawaf#1567

Signed-off-by: majiayu000 <1835304752@qq.com>
Add test verifying HIGHEST_SEVERITY defaults to 255 (ModSecurity
sentinel) when no rules with severity have fired.

Add TODO comment documenting that rules without an explicit severity
action have Severity_=0 (Go zero value = Emergency), which may
incorrectly update HIGHEST_SEVERITY.

Signed-off-by: majiayu000 <1835304752@qq.com>
@majiayu000 majiayu000 marked this pull request as ready for review March 23, 2026 14:57
@majiayu000 majiayu000 requested a review from a team as a code owner March 23, 2026 14:57
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 23, 2026

Codecov Report

❌ Patch coverage is 77.77778% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.67%. Comparing base (febb510) to head (136d21a).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/corazawaf/transaction.go 71.42% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1569      +/-   ##
==========================================
- Coverage   86.68%   86.67%   -0.02%     
==========================================
  Files         179      179              
  Lines        8790     8794       +4     
==========================================
+ Hits         7620     7622       +2     
- Misses        911      912       +1     
- Partials      259      260       +1     
Flag Coverage Δ
coraza.no_memoize 86.75% <77.77%> (-0.02%) ⬇️
coraza.rule.case_sensitive_args_keys 86.63% <77.77%> (-0.02%) ⬇️
coraza.rule.mandatory_rule_id_check 86.66% <77.77%> (-0.02%) ⬇️
coraza.rule.multiphase_evaluation 86.39% <77.77%> (-0.02%) ⬇️
coraza.rule.no_regex_multiline 86.62% <77.77%> (-0.02%) ⬇️
coraza.rule.rx_prefilter 86.66% <77.77%> (-0.02%) ⬇️
default 86.67% <77.77%> (-0.02%) ⬇️
examples+ 17.18% <11.11%> (-0.09%) ⬇️
examples+coraza.no_memoize 84.59% <77.77%> (-0.02%) ⬇️
examples+coraza.rule.case_sensitive_args_keys 84.59% <77.77%> (-0.02%) ⬇️
examples+coraza.rule.mandatory_rule_id_check 84.70% <77.77%> (-0.02%) ⬇️
examples+coraza.rule.multiphase_evaluation 86.39% <77.77%> (-0.02%) ⬇️
examples+coraza.rule.no_regex_multiline 84.52% <77.77%> (-0.02%) ⬇️
examples+coraza.rule.rx_prefilter 84.67% <77.77%> (-0.02%) ⬇️
examples+no_fs_access 84.04% <77.77%> (-0.02%) ⬇️
ftw 86.67% <77.77%> (-0.02%) ⬇️
no_fs_access 86.02% <77.77%> (-0.02%) ⬇️
tinygo 86.66% <77.77%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Member

@M4tteoP M4tteoP left a comment

Choose a reason for hiding this comment

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

Please see also #1567 (comment)

Address review feedback: replace hardcoded "255" with a
defaultHighestSeverity const and add ModSecurity v2/v3
source references.

Signed-off-by: majiayu000 <1835304752@qq.com>
@majiayu000
Copy link
Copy Markdown
Author

Hi @M4tteoP, here are the specific ModSecurity v2 and v3 source references confirming the 255 default and comparison logic:

ModSec v2 (v2/master)

Default = 255: apache2/modsecurity.c#L550

msr->highest_severity = 255; /* high, invalid value */

Comparison (lower number = higher severity): apache2/re.c#L2745-L2749

if ((acting_actionset->severity > 0) && (acting_actionset->severity < msr->highest_severity)
    && !rule->actionset->is_chained) {
    msr->highest_severity = acting_actionset->severity;
}

ModSec v3 (v3/master)

Default = 255: src/transaction.cc#L124

m_highestSeverityAction(255),

Comparison (lower number = higher severity): src/actions/severity.cc#L74-L86

if (transaction->m_highestSeverityAction > this->m_severity) {
    transaction->m_highestSeverityAction = this->m_severity;
}

Both v2 and v3 are aligned: default 255 (sentinel for "no severity set"), lower number = higher severity, comparison uses < (v2) / > (v3, checking from the other direction) to keep the most severe value.

Note: v2 has an additional severity > 0 guard that skips emergency (0). Our current implementation doesn't have this guard — happy to add it if you think we should match that behavior exactly.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Default HIGHEST_SEVERITY is set to "255"; Transaction now initializes the variable to that value and only updates it when a matched rule explicitly sets a severity (marked by a new HasSeverity_ flag). Severity parsing/compare logic was adjusted to use numeric comparison with a parse fallback.

Changes

Cohort / File(s) Summary
Severity update logic
internal/corazawaf/transaction.go
Transaction.MatchRule now parses stored highest severity with strconv.Atoi (fallback 255) and updates tx.variables.highestSeverity only when the matched rule has HasSeverity_, using numeric comparison.
Default initialization
internal/corazawaf/waf.go
Introduce defaultHighestSeverity = "255" and set new Transaction instances' tx.variables.highestSeverity to that value instead of "0".
Rule metadata
internal/corazarules/rule.go
Add HasSeverity_ bool to RuleMetadata to mark rules that explicitly define a severity.
Severity action
internal/actions/severity.go
severityFn.Init now sets the target rule's HasSeverity_ = true in addition to assigning Severity_.
Tests
testing/engine/rulemetadata.go
Add three test profiles validating default highest severity = 255, that non-severity actions don't poison highest severity, and that the lowest numeric severity among fired rules becomes HIGHEST_SEVERITY.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐇 I set the highest to two-five-five,
The smallest number now takes the prize,
A little flag says "this one counts,"
Rules hop by in tidy amounts,
ModSec manners, soft and wise.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: aligning HIGHEST_SEVERITY with ModSecurity behavior through default value and comparison logic fixes.
Linked Issues check ✅ Passed All requirements from issue #1567 are met: HIGHEST_SEVERITY defaults to 255 [#1567], comparison logic corrected for lower=higher-severity [#1567], HasSeverity_ prevents zero-value poisoning [#1567], and ModSecurity behavior alignment is documented [#1567].
Out of Scope Changes check ✅ Passed All changes are in-scope: modifications to transaction.go, waf.go, severity.go, rule.go, and test coverage directly implement the fixes for issue #1567; no unrelated changes detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/corazawaf/transaction.go`:
- Around line 558-566: Highest-severity tracking is poisoned by rules that omit
severity because Severity_ defaults to 0; change the logic to only consider a
rule's severity when it was explicitly set: add a boolean marker on the rule
metadata (e.g., HasSeverity on RuleMetadata or make Severity_ an optional type)
and update the comparison around tx.variables.highestSeverity
(hs.Get()/hs.Set()) to skip rules that do not have HasSeverity true; initialize
the highest-severity sentinel to a value that cannot be a real severity and
ensure the update uses r.Severity_.Int() only when the rule’s HasSeverity flag
is true so a later explicit severity can win (also update any rule
creation/parsing paths that populate HasSeverity).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b0f7af28-bc26-464a-98a7-40837357fde1

📥 Commits

Reviewing files that changed from the base of the PR and between 85b20e0 and aa18c8c.

📒 Files selected for processing (3)
  • internal/corazawaf/transaction.go
  • internal/corazawaf/waf.go
  • testing/engine/rulemetadata.go

Rules without a severity action have Severity_=0 (Go zero value),
which is indistinguishable from RuleSeverityEmergency. This poisoned
HIGHEST_SEVERITY by writing 0 on the first no-severity match, making
later explicit severities unable to win.

Add HasSeverity_ flag to RuleMetadata, set it in the severity action,
and only update HIGHEST_SEVERITY when the flag is true. This mirrors
ModSecurity v3 which checks m_severity > -1 before updating.

Adds regression test: no-severity rule followed by severity:5 rule
must result in HIGHEST_SEVERITY=5, not 0.

Signed-off-by: majiayu000 <1835304752@qq.com>
@majiayu000
Copy link
Copy Markdown
Author

@M4tteoP The const comment now references both ModSec v2 (apache2/msc_util.c, highest_severity = 255) and v3 (src/transaction.cc, m_highestSeverity = 255). Both versions use 255 as the "no severity set" sentinel, so they're aligned on this.

Also just pushed a fix for the zero-value poisoning issue — rules without an explicit severity action were writing 0 into HIGHEST_SEVERITY. Added a HasSeverity_ flag on RuleMetadata so only rules that actually set severity participate in the comparison. This mirrors ModSec v3's m_severity > -1 guard in transaction.cc.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/corazawaf/transaction.go`:
- Around line 562-564: The code calls strconv.Atoi(hs.Get()) and ignores the
returned error; update the block around hs.Get()/strconv.Atoi to check the error
returned by strconv.Atoi and handle it (e.g., log and/or return, or fall back to
a safe default) before comparing with r.Severity_.Int(); specifically modify the
logic that assigns currentVal and uses hs.Set(strconv.Itoa(...)) so that
strconv.Atoi's error is checked and handled consistently with project guidelines
while preserving existing behavior of hs.Set and the comparison with
r.Severity_.Int().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 24a740d9-f0e4-447b-a37f-5743583663fd

📥 Commits

Reviewing files that changed from the base of the PR and between aa18c8c and 6b81ee2.

📒 Files selected for processing (4)
  • internal/actions/severity.go
  • internal/corazarules/rule.go
  • internal/corazawaf/transaction.go
  • testing/engine/rulemetadata.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • testing/engine/rulemetadata.go

Fall back to sentinel value 255 if Atoi fails, so any real
severity can still win. In practice this path is unreachable
since the variable is always set via strconv.Itoa, but handle
it explicitly for correctness.

Signed-off-by: majiayu000 <1835304752@qq.com>
@fzipi
Copy link
Copy Markdown
Member

fzipi commented Apr 1, 2026

@majiayu000 Can you apply go fmt?

Signed-off-by: majiayu000 <1835304752@qq.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/corazarules/rule.go (1)

22-24: Document HasSeverity_ semantics inline.

HasSeverity_ changes rule-metadata semantics, but without a field comment it’s easy to misread false as severity 0 instead of “unset”. Please add an inline comment clarifying that it tracks whether severity was explicitly configured.

Proposed doc-only patch
 	Severity_    types.RuleSeverity
+	// HasSeverity_ reports whether the rule explicitly set a severity action.
+	// This distinguishes an unset value from RuleSeverityEmergency (0).
 	HasSeverity_ bool
 	Version_     string

Based on learnings: Document any intentional deviations from ModSecurity compatibility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/corazarules/rule.go` around lines 22 - 24, Add an inline comment on
the HasSeverity_ field clarifying its semantics: explain that HasSeverity_ is a
boolean sentinel indicating whether Severity_ was explicitly configured (true)
versus unset (false), and that a false value does not mean severity zero;
reference the Severity_ and HasSeverity_ fields in the comment and note any
intentional ModSecurity compatibility deviation if applicable so readers
understand the difference between "unset" and an actual severity value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/corazarules/rule.go`:
- Around line 22-24: Add an inline comment on the HasSeverity_ field clarifying
its semantics: explain that HasSeverity_ is a boolean sentinel indicating
whether Severity_ was explicitly configured (true) versus unset (false), and
that a false value does not mean severity zero; reference the Severity_ and
HasSeverity_ fields in the comment and note any intentional ModSecurity
compatibility deviation if applicable so readers understand the difference
between "unset" and an actual severity value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ee1eadff-65ce-4342-a078-0d285140a68f

📥 Commits

Reviewing files that changed from the base of the PR and between 06fb118 and 79f7923.

📒 Files selected for processing (1)
  • internal/corazarules/rule.go

Copy link
Copy Markdown
Member

@fzipi fzipi left a comment

Choose a reason for hiding this comment

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

Probably it is better to add this to the types/severity.go instead?

// https://github.com/owasp-modsecurity/ModSecurity/blob/v2/master/apache2/msc_util.c
// - ModSec v3: src/transaction.cc m_highestSeverity initialized to 255
// https://github.com/owasp-modsecurity/ModSecurity/blob/v3/master/src/transaction.cc
defaultHighestSeverity = "255"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would use an int here instead

Suggested change
defaultHighestSeverity = "255"
defaultHighestSeverity = 255

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks, will change defaultHighestSeverity to int and remove the strconv roundtrip.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Take a look at this one instead: #1569 (comment)

@fzipi
Copy link
Copy Markdown
Member

fzipi commented Apr 1, 2026

Suggestion: encode "unset" in types/severity.go instead of a separate bool

The current approach uses a HasSeverity_ bool on the Rule struct to distinguish "severity not set" from "severity = 0 (Emergency)". This works, but has a couple of downsides:

  1. Two fields to keep in sync — every code path that sets Severity_ must also set HasSeverity_ = true. If someone adds a new code path and forgets the bool, it silently breaks.
  2. The sentinel 255 is a magic constant defined only in waf.go, disconnected from the type it relates to.

Alternative: use a sentinel value in the type itself

// In types/severity.go
const (
    // RuleSeverityUnset means no severity was assigned to this rule.
    // Aligns with ModSecurity's sentinel (255 in v2/v3).
    RuleSeverityUnset RuleSeverity = -1

    RuleSeverityEmergency RuleSeverity = 0
    // ... rest unchanged
)

Then:

  • Drop HasSeverity_ — initialize Severity_ to RuleSeverityUnset instead. The severity action just sets the value; no bool needed.
  • Comparison in transaction.go simplifies to checking r.Severity_ != RuleSeverityUnset before updating.
  • The defaultHighestSeverity constant ("255") could stay in waf.go or be exposed from the types package.

Key win: one field instead of two, and the type system makes the invariant self-documenting. Using -1 (outside the valid 0–7 range) is idiomatic for "not set" and eliminates the risk of the bool getting out of sync.

@fzipi fzipi added the fix Fixes something broken label Apr 1, 2026
Signed-off-by: majiayu000 <1835304752@qq.com>
Signed-off-by: majiayu000 <1835304752@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Fixes something broken

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HIGHEST_SEVERITY variable: implementation likely not aligned to modsec

3 participants