Skip to content
Open
1 change: 1 addition & 0 deletions internal/actions/severity.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func (a *severityFn) Init(r plugintypes.RuleMetadata, data string) error {
return err
}
r.(*corazawaf.Rule).Severity_ = sev
r.(*corazawaf.Rule).HasSeverity_ = true
return nil
}

Expand Down
19 changes: 11 additions & 8 deletions internal/corazarules/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ type RuleMetadata struct {
Line_ int
Rev_ string
Severity_ types.RuleSeverity
Version_ string
Tags_ []string
Maturity_ int
Accuracy_ int
Operator_ string
Phase_ types.RulePhase
Raw_ string
SecMark_ string
// HasSeverity_ reports whether the rule explicitly set a severity action.
// This distinguishes an unset value from RuleSeverityEmergency (0).
HasSeverity_ bool
Version_ string
Tags_ []string
Maturity_ int
Accuracy_ int
Operator_ string
Phase_ types.RulePhase
Raw_ string
SecMark_ string
// Contains the Id of the parent rule if you are inside
// a chain. Otherwise, it will be 0
ParentID_ int
Expand Down
17 changes: 13 additions & 4 deletions internal/corazawaf/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,10 +555,19 @@ func (tx *Transaction) MatchRule(r *Rule, mds []types.MatchData) {
}

// set highest_severity
hs := tx.variables.highestSeverity
maxSeverity, _ := types.ParseRuleSeverity(hs.Get())
if r.Severity_ > maxSeverity {
hs.Set(strconv.Itoa(r.Severity_.Int()))
// Only update when severity was explicitly set via the severity action.
// This mirrors ModSecurity v3 behavior (m_severity > -1 check in transaction.cc).
if r.HasSeverity_ {
hs := tx.variables.highestSeverity
currentVal := defaultHighestSeverity
if v := hs.Get(); v != "" {
if parsed, err := strconv.Atoi(v); err == nil {
currentVal = parsed
}
}
if r.Severity_.Int() < currentVal {
hs.Set(strconv.Itoa(r.Severity_.Int()))
}
}

mr := &corazarules.MatchedRule{
Expand Down
10 changes: 9 additions & 1 deletion internal/corazawaf/waf.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ var wafIDCounter atomic.Uint64
const (
// DefaultRequestBodyJsonDepthLimit is the default limit for the depth of JSON objects in the request body
DefaultRequestBodyJsonDepthLimit = 1024

// defaultHighestSeverity is the default value for HIGHEST_SEVERITY when no rules
// with severity have been matched. Value 255 aligns with ModSecurity behavior:
// - ModSec v2: apache2/msc_util.c highest_severity initialized to 255
// 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
)

// WAF instance is used to store configurations and rules
Expand Down Expand Up @@ -259,7 +267,7 @@ func (w *WAF) newTransaction(opts Options) *Transaction {
tx.variables.reqbodyProcessorError.Set("0")
tx.variables.requestBodyLength.Set("0")
tx.variables.duration.Set("0")
tx.variables.highestSeverity.Set("0")
tx.variables.highestSeverity.Set(strconv.Itoa(defaultHighestSeverity))
tx.variables.uniqueID.Set(tx.id)
tx.setTimeVariables()

Expand Down
85 changes: 85 additions & 0 deletions testing/engine/rulemetadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,88 @@ SecAction "id:1, log, severity:5"
SecRule HIGHEST_SEVERITY "@eq 5" "id:2, log"
`,
})

var _ = profile.RegisterProfile(profile.Profile{
Meta: profile.Meta{
Author: "majiayu000",
Description: "Test HIGHEST_SEVERITY defaults to 255 when no rules with severity fire",
Enabled: true,
Name: "rulemetadata_default_severity.yaml",
},
Tests: []profile.Test{
{
Title: "highest_severity_default",
Stages: []profile.Stage{
{
Stage: profile.SubStage{
Output: profile.ExpectedOutput{
TriggeredRules: []int{1},
},
},
},
},
},
},
Rules: `
SecRule HIGHEST_SEVERITY "@eq 255" "id:1, log"
`,
})

// Regression: a rule without severity must not poison HIGHEST_SEVERITY.
// Without the HasSeverity_ guard, the no-severity rule's zero value (0) would
// win over a later explicit severity:5, leaving HIGHEST_SEVERITY stuck at 0.
var _ = profile.RegisterProfile(profile.Profile{
Meta: profile.Meta{
Author: "majiayu000",
Description: "Test that rules without severity do not poison HIGHEST_SEVERITY",
Enabled: true,
Name: "rulemetadata_no_severity_poison.yaml",
},
Tests: []profile.Test{
{
Title: "no_severity_then_explicit",
Stages: []profile.Stage{
{
Stage: profile.SubStage{
Output: profile.ExpectedOutput{
TriggeredRules: []int{1, 2, 3},
},
},
},
},
},
},
Rules: `
SecAction "id:1, log"
SecAction "id:2, log, severity:5"
SecRule HIGHEST_SEVERITY "@eq 5" "id:3, log"
`,
})

var _ = profile.RegisterProfile(profile.Profile{
Meta: profile.Meta{
Author: "majiayu000",
Description: "Test HIGHEST_SEVERITY with multiple severities keeps the lowest number",
Enabled: true,
Name: "rulemetadata_highest_severity.yaml",
},
Tests: []profile.Test{
{
Title: "highest_severity_multiple",
Stages: []profile.Stage{
{
Stage: profile.SubStage{
Output: profile.ExpectedOutput{
TriggeredRules: []int{1, 2, 3},
},
},
},
},
},
},
Rules: `
SecAction "id:1, log, severity:5"
SecAction "id:2, log, severity:2"
SecRule HIGHEST_SEVERITY "@eq 2" "id:3, log"
`,
})