ADR-0014: `SecDefaultAction` constraints
- Status: proposed
- Date: 2026-10-06
- Deciders: @fzipi
- Category: Divergence
Context
Checks each engine applies to a SecDefaultAction action list (all verified against
source on 2026-10-06):
| Check | ModSecurity v2 (cmd_default_action) | libmodsecurity v3 (seclang-parser.yy) | Coraza 3.8.1 (rule_parser.go ParseDefaultActions) |
|---|---|---|---|
| missing disruptive action | error | error | error |
missing phase | error | defaults to phase 1 | error |
metadata actions (id, rev, msg, severity, ver, accuracy, maturity, logdata) | error | error (not run-time actions) | error |
tag | not checked (ENH comment) | error | error |
chain, skip, skipAfter | error | error | error |
t:none / transformations | warning | error on t:none | error on any t: |
second SecDefaultAction for the same phase | replaces the first | error | error |
| when the check runs | at the directive | at the directive | at the next rule parsed |
Coraza also installs a built-in phase:2,log,auditlog,pass when none is configured.
ModSecurity v2 keeps a single current default action set rather than one per phase,
which is what let a phase-less rule inherit its phase (ADR-0017).
ModSecurity v2 keeps a single current default action set whose phase a phase-less rule
inherits; the other two apply default actions by the rule’s own phase (see ADR-0017).
Decision
A SecDefaultAction MUST contain a phase and exactly one disruptive action, and MUST
NOT contain chain, skip, skipAfter, any transformation (including t:none) or the
metadata actions id, rev, msg, tag, severity, ver, accuracy, maturity,
logdata. At most one SecDefaultAction per phase MAY appear in one configuration
context; a second one for the same phase MUST be a configuration error. Violations MUST
be reported when the directive is parsed, whether or not a rule follows it. Normative
text: spec/03-processing-model.md#default-actions.
Options considered
- v2 replacement semantics for a repeated phase: one engine against two, and silently changing the default for later rules is a frequent misconfiguration; rejected.
- Error on a repeated phase (chosen): v3 and Coraza behaviour; CRS complies (one per
phase in
crs-setup.conf). - Allow
tagas v2 does: a tag inherited by every rule defeatsctl:ruleRemoveByTag; rejected.
Consequences
- For ModSecurity v2: reject
tag; reject a repeated phase (today it replaces, sodefault-action-redefined.yamlfails on v2). - For libmodsecurity v3: require
phaseinstead of defaulting to 1 (todaydefault-action-no-phase.yamlfails on v3). - For Coraza: validate at the directive, not at the next rule (today the two negative tests pass only because a rule follows the directive).
- For rule authors:
SecDefaultAction "phase:N,log,auditlog,pass"as CRS ships it is valid everywhere.
Tests
tests/engine/processing/default-action-no-phase.yamltests/engine/processing/default-action-no-disruptive.yamltests/engine/processing/default-action-redefined.yaml