Skip to content

Emit deny action inline when includeEngineConfig=false - #81

Open
cognitivegears wants to merge 1 commit into
mainfrom
fix/explicit-deny-when-engine-config-omitted
Open

Emit deny action inline when includeEngineConfig=false#81
cognitivegears wants to merge 1 commit into
mainfrom
fix/explicit-deny-when-engine-config-omitted

Conversation

@cognitivegears

Copy link
Copy Markdown
Owner

Problem

OAShield's generated rules use the block action, whose disruptive behavior is inherited from the SecDefaultAction emitted in mainconfig.conf. With includeEngineConfig=false (the documented way to layer the rules onto an engine that already configures itself), that SecDefaultAction is omitted — so block inherits the host engine's phase-2 default.

The OWASP Core Rule Set defaults phase 2 to pass (it uses anomaly scoring with an explicit deny only in rule 949110). So when OAShield rules are layered onto a Coraza/ModSecurity + CRS deployment with includeEngineConfig=false, every OAShield rule silently no-ops: it logs but never denies. A request that violates the API contract (undeclared parameter, shadow endpoint, bad type) is logged and then forwarded to the app.

Repro: generate with includeEngineConfig=false, load the rules into a stock Coraza+CRS reverse proxy, POST a body with an undeclared property → CRS forwards it, and OAShield logs Unknown parameter detected but returns 200 instead of 403.

Fix

Introduce a ruleDisruptiveAction template variable:

  • includeEngineConfig=true (default) → block (unchanged; rules inherit the emitted SecDefaultAction).
  • includeEngineConfig=false → the disruptive action inline (deny,status:N / drop / pass / redirect, driven by the existing denyAction/denyStatus/denyRedirectUrl), so the rules block regardless of the host's default action — or, with denyAction=pass, run detection-only.

This is the natural meaning of the two options together: if you're not emitting the engine config, the rules can't rely on it, so they carry the action themselves.

Compatibility

Default output is unchanged — GoldenFileTest (all sample specs × both flavors) passes with no golden churn, because ruleDisruptiveAction is still block whenever the engine config is emitted.

Tests

DenyConfigTest gains three cases (all green, mvn test -Dtest=DenyConfigTest,GoldenFileTest → 24 run, 0 failures):

  • includeEngineConfig=false → operation rules and the catch-all default-deny carry inline deny,status:403, no bare block.
  • includeEngineConfig=false + denyAction=pass → inline pass (detection-only).
  • default → bare block preserved.

Docs

docs/configuration.md updated to describe the inline-action behavior under includeEngineConfig=false.

🤖 Generated with Claude Code

Generated rules use the `block` action, which inherits the disruptive action
from the SecDefaultAction emitted in mainconfig.conf. With
includeEngineConfig=false that SecDefaultAction is omitted, so `block` inherits
the host engine's phase-2 default. A host ruleset such as the OWASP Core Rule
Set defaults phase 2 to `pass`, so the layered OAShield rules silently no-op:
they log but never deny.

When includeEngineConfig=false, emit the disruptive action inline on each rule
(deny,status:N / drop / pass / redirect, driven by denyAction/denyStatus) via a
new `ruleDisruptiveAction` template variable, so the rules block regardless of
the host's default action. When the engine config is included (the default),
rules keep the bare `block` and output is unchanged, so golden snapshots are
unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019tqxm2rpR6mhJdodg7TbU4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant