From 7924a94416b1cf6d0381c3adda1d02ab95b1a275 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Mon, 26 Oct 2026 10:50:49 -0300 Subject: [PATCH] Clarify when chained-rule actions run and how updates affect chains The chain entry said non-disruptive actions run when "the rule that contains them matches", which reads as if a later rule could fire on its own. Chained rules are evaluated in order and stop at the first non-match, so an action runs once its rule and every rule before it matched. Add a worked example. SecRuleUpdateActionById now collects the chain caveats in one place: v2 takes the chain flag from the update (so chain must be repeated, and must not be added to a rule without it), v3 applies updates at run time to the chain starter only and runs every updated action, setvar included, only on a full chain match. Refs owasp-modsecurity/ModSecurity#2860 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RiUJuqXNvvpytiGRiDFyTy --- .../3-2-configuration-directives.md | 34 +++++++++++++++++-- content/3-reference-manual/3-5-actions.md | 33 +++++++++++++++++- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/content/3-reference-manual/3-2-configuration-directives.md b/content/3-reference-manual/3-2-configuration-directives.md index dbb2d08..4376adc 100644 --- a/content/3-reference-manual/3-2-configuration-directives.md +++ b/content/3-reference-manual/3-2-configuration-directives.md @@ -1228,11 +1228,39 @@ SecRule ARGS "@rx attack" \ The addition of t:none will neutralize any previous transformation functions specified (t:lowercase, in the example). -{{% notice note %}} +### Updating chained rules -In ModSecurity v2, if the target rule is a chained rule, you must currently specify chain in the SecRuleUpdateActionById action list as well. In libModSecurity v3, action updates to a chained rule may only be made to the main (first) rule in the chain, and the `:offset` suffix is not supported (the ID is parsed as a number). +The two engines handle updates to rules that are part of a [chain]({{% relref "3-reference-manual/3-5-actions#chain" %}}) differently. -{{% /notice %}} +**ModSecurity v2** merges the new actions into the rule itself, and the merge always takes the chain flag from the new action list: + +- If the target rule has the chain action, **repeat chain in the update**. Otherwise the update removes the rule from the chain: the rules that followed it are then evaluated on their own, and their actions run even when the original chain would not have matched. +- Do not add chain when updating a rule that does not have it, such as the last rule of a chain. It has been reported that the msg of a chain updated this way is no longer logged. +- `RULEID:offset` targets a rule inside a chain: offset 0 (the default) is the chain starter, 1 the rule after it, and so on. + +``` seclang +SecRule REQUEST_HEADERS:User-Agent "@rx ^curl" \ + "id:12346,\ + phase:2,\ + pass,\ + log,\ + msg:'Example chain',\ + chain" + SecRule REQUEST_HEADERS:Accept "@rx \*/\*" \ + "t:none" + +# Wrong in v2: unchains rule 12346, so the Accept rule runs on its own +SecRuleUpdateActionById 12346 "deny,status:403" + +# Right in v2: the chain is kept +SecRuleUpdateActionById 12346 "deny,status:403,chain" +``` + +**libModSecurity v3** keeps the rule unchanged and applies the updated actions at run time, whenever the rule with that ID is evaluated: + +- Only the chain starter can be updated, since the other rules of a chain have no ID. The `:offset` suffix is not supported (the ID is parsed as a number). +- The chain is never broken, so there is no need to repeat chain. If chain is repeated anyway, for configurations shared with v2, it is ignored and a "General failure" message is printed when the rules are loaded. +- Every action added by the update, including setvar, runs only when the whole chain matches. This is different from a setvar written on the chain starter itself, which runs as soon as the chain starter matches. ## SecRuleUpdateTargetById diff --git a/content/3-reference-manual/3-5-actions.md b/content/3-reference-manual/3-5-actions.md index 20f941a..84aa152 100644 --- a/content/3-reference-manual/3-5-actions.md +++ b/content/3-reference-manual/3-5-actions.md @@ -229,9 +229,40 @@ The following directives can be used in rule chains: Special rules control the usage of actions in chained rules: - Any actions that affect the rule flow (i.e., the disruptive actions, skip and skipAfter) can be used only in the chain starter. They will be executed only if the entire chain matches. -- Non-disruptive actions can be used in any rule; they will be executed if the rule that contains them matches and not only when the entire chain matches. In libModSecurity v3 this is true only for setvar: every other non-disruptive action in a chained rule (ctl, setenv, exec, log, ...) runs only once the whole chain has matched. +- Non-disruptive actions can be used in any rule of the chain. The rules of a chain are evaluated in order, and evaluation stops at the first rule that does not match. A non-disruptive action therefore runs once the rule that contains it **and every rule before it** in the chain have matched; the rules after it play no part. The engines differ in which actions this applies to: + - **ModSecurity v2**: every non-disruptive action (setvar, ctl, setenv, exec, ...). + - **libModSecurity v3**: only setvar. Every other non-disruptive action in a chained rule (ctl, setenv, exec, ...) runs only once the whole chain has matched. - The metadata actions (e.g., id, rev, msg) can be used only in the chain starter. +Example, with a request whose User-Agent starts with `curl`, whose Accept header is `*/*` and whose Host is `localhost`: + +``` seclang +SecAction \ + "id:106,\ + phase:1,\ + pass,\ + nolog,\ + setvar:'tx.counter=0'" + +SecRule REQUEST_HEADERS:User-Agent "@rx ^curl" \ + "id:107,\ + phase:2,\ + pass,\ + log,\ + msg:'Example chain',\ + setvar:'tx.counter=+1',\ + chain" + SecRule REQUEST_HEADERS:Accept "@rx \*/\*" \ + "setvar:'tx.counter=+2',\ + chain" + SecRule REQUEST_HEADERS:Host "@rx ^example\.com$" \ + "setvar:'tx.counter=+4'" +``` + +The first two rules match and the third does not, so the chain as a whole does not match, but `tx.counter` ends up as 3: the setvar actions of the first two rules ran. If the User-Agent did not start with `curl`, `tx.counter` would stay 0 even though the Accept rule would have matched, because the rest of the chain is never evaluated once a rule fails. + +Take extra care when changing the actions of a chained rule with SecRuleUpdateActionById: the two engines treat chains differently there, see [SecRuleUpdateActionById]({{% relref "3-reference-manual/3-2-configuration-directives#secruleupdateactionbyid" %}}). + ## ctl Changes ModSecurity configuration on transient, per-transaction basis. Any changes made using this action will affect only the transaction in which the action is executed. The default configuration, as well as the other transactions running in parallel, will be unaffected.