Skip to content

fix(waf): push rule suppressions to nodes as SecRuleRemoveById - #15

Merged
marcoome merged 1 commit into
mainfrom
fix/waf-suppressions-to-nodes
Sep 15, 2026
Merged

marcoome merged 1 commit into
mainfrom
fix/waf-suppressions-to-nodes

Conversation

@marcoome

Copy link
Copy Markdown
Contributor

Fixes #14

Root cause

The report describes a hung EventSource, but the SSE stream itself is fine. Reproduced with a local Beszel 0.19.0 hub behind caddy:2.11.4 + coraza-caddy v2.5.0 (same pins as the edge image), driven by headless Chrome:

  • detection-only: dashboard loads, GET /api/realtime 200, subscription POST /api/realtime 204
  • blocking: GET 200, POST 403, console: Failed to subscribe to systems collection: ClientResponseError 403

The POST body carries PocketBase subscription topics like systems/*?options=.... CRS 942100 (libinjection SQLi) treats /* as a SQL comment, the anomaly score reaches the blocking threshold, and the SDK reconnects forever, which looks like a stuck EventSource. Plain "systems" passes, "systems/*" is blocked.

Fix

The panel already had a "suppress rule" UI, but it only hid events - it never reached the node config, so in blocking mode the rule kept firing.

  • internal/domain/routes/waf_suppress.go: active suppressions (global + per-route) are rendered as SecRuleRemoveById <id> and appended to every WAF-enabled route's directives, after the CRS include (where SecRuleRemoveById must sit, see [WAF] Custom instructions not seems to be applied #9).
  • Rule IDs are whitelisted (NNN or NNN-MMM) before reaching SecLang - the form stores free text.
  • Saving or deleting a suppression schedules a push to all nodes.
  • Docs: docs/WAF.md gets a "Blocking mode and JSON API false positives" section with the Beszel/PocketBase case and the manual directive.

Verification

  • go test ./internal/domain/routes/ ./internal/caddyapi/ ./internal/wafevents/ ./internal/httpserver/... green; new unit test covers global/per-route scoping, dedupe, ranges, and an injection attempt (942100; SecRuleEngine Off is dropped).
  • Browser E2E in blocking mode with SecRuleRemoveById 942100 in the generated directives: GET 200, POST 204, 204, no console errors.

Suppressing a rule in Admin -> Security -> WAF only hid its events; in
blocking mode the node kept answering 403. Active suppressions (global
and per-route) are now appended to every WAF-enabled route's directives
after the CRS include, and saving or deleting one re-pushes all nodes.
Rule IDs are whitelisted (NNN or NNN-MMM) before they reach SecLang since
the form stores free text.

Root cause of the report: not SSE. Beszel's dashboard opens /api/realtime
fine, then POSTs subscription topics like "systems/*"; CRS 942100
(libinjection) reads "/*" as a SQL comment, the anomaly score hits the
threshold and the POST gets 403, so the SDK reconnects forever. Verified
in headless Chrome against a local Beszel hub behind caddy+coraza v2.5.0:
blocking mode fails, blocking mode + SecRuleRemoveById 942100 passes.
Documented in docs/WAF.md.

Fixes #14
@marcoome
marcoome merged commit dc2aa47 into main Sep 15, 2026
2 checks passed
@marcoome
marcoome deleted the fix/waf-suppressions-to-nodes branch September 15, 2026 15:52
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.

[WAF] The WAF breaks the EventSource (SSE) compatibility

1 participant