When allowWebsocket=true and WAF is enabled, the WAF handler sits first
in the handler chain and processes the initial HTTP upgrade request
(GET + Upgrade: websocket). If any rule matches, Coraza can block the
handshake before SecAuditEngine captures it — producing no log entry
and an unexplained connection failure from the client's perspective.
Fix: when allowWebsocket=true, prepend a phase:1 SecLang rule that
matches Upgrade: websocket (case-insensitive) and turns the rule engine
off for that transaction via ctl:ruleEngine=off. After the 101
Switching Protocols response the connection becomes a raw WebSocket
tunnel that the WAF cannot inspect anyway, so this bypass has no impact
on normal HTTP traffic through the same host.
The rule is inserted before OWASP CRS includes so it always fires first
regardless of which ruleset is loaded.
Add 9 unit tests in caddy-waf.test.ts covering: bypass present/absent,
phase:1 placement, case-insensitive regex, nolog/noauditlog flags,
ordering before CRS, and compatibility with custom directives.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug: when a proxy host had per-host WAF explicitly disabled (enabled:false)
with waf_mode:"merge" (or no waf_mode set), resolveEffectiveWaf entered the
merge branch and returned enabled:true unconditionally, applying the global
WAF to a host the user had opted out of.
Fix: add `if (host.enabled === false) return null` at the top of the merge
branch. Explicit opt-out now takes precedence over the global setting
regardless of mode. The override mode already handled this correctly.
Also extract resolveEffectiveWaf from caddy.ts into caddy-waf.ts so it
can be unit tested. Add 12 new tests covering no-config fallback,
merge opt-out regression, merge settings combination, and override mode.
What runs without OWASP CRS: only SecRuleEngine + audit directives +
any custom_directives. The @coraza.conf-recommended and CRS includes
are gated behind load_owasp_crs (fixed in previous commit).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The WAF handler always prepended 'Include @coraza.conf-recommended' to the
SecLang directives regardless of load_owasp_crs. The @-prefixed paths only
resolve from the embedded coraza-coreruleset filesystem, which the Caddy
WAF plugin mounts only when load_owasp_crs=true. Without it Caddy fails:
"failed to readfile: open @coraza.conf-recommended: no such file or directory"
Fix: gate all @-prefixed Include directives behind load_owasp_crs.
Also extract buildWafHandler from caddy.ts into caddy-waf.ts so it can be
unit tested in isolation, and add tests/unit/caddy-waf.test.ts (19 tests)
covering the regression, CRS include ordering, excluded rule IDs, and
handler structure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>