Skip to content

🔥 feat: Add Coraza middleware#1759

Open
GoFurry wants to merge 10 commits intogofiber:mainfrom
GoFurry:pr/coraza-v3
Open

🔥 feat: Add Coraza middleware#1759
GoFurry wants to merge 10 commits intogofiber:mainfrom
GoFurry:pr/coraza-v3

Conversation

@GoFurry
Copy link
Copy Markdown

@GoFurry GoFurry commented Mar 28, 2026

This PR adds a new coraza middleware module for Fiber v3 under v3/coraza.

It integrates the Coraza WAF engine as a Fiber middleware and is designed to stay aligned with the existing gofiber/contrib middleware style:

  • main entrypoint via coraza.New(config ...Config) fiber.Handler
  • optional advanced lifecycle control via coraza.NewEngine(Config)
  • no package-level global singleton state
  • no built-in admin HTTP routes
  • data-oriented observability methods for advanced use cases

Features

  • request header inspection
  • request body inspection
  • configurable block and error handlers
  • optional RootFS support for directives files
  • explicit reload support through Engine.Reload()
  • lightweight in-memory metrics snapshots
  • lifecycle snapshots and reports for advanced integrations

Notes

  • response body inspection is not supported
  • coraza.New() starts without external rule files by default; rules are loaded when DirectivesFile is configured
  • invalid startup configuration causes coraza.New(...) to panic, which matches fail-fast startup expectations for middleware initialization

Validation

Tested with:

go test ./...
go test -race ./...


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Coraza WAF middleware for Fiber v3: configurable blocking, custom block/error handlers, engine lifecycle (init/reload), per-instance isolation, request inspection controls, and built-in metrics/snapshots/reporting.

* **Documentation**
  * Added detailed Coraza middleware docs with installation, configuration and usage examples, observability notes, and a top-level README entry with CI status badge.

* **Tests**
  * New test suite covering init, blocking, header/body inspection, metrics, lifecycle, error/panic recovery, and extensibility.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@GoFurry GoFurry requested a review from a team as a code owner March 28, 2026 14:50
@GoFurry GoFurry requested review from ReneWerner87, efectn, gaby and sixcolors and removed request for a team March 28, 2026 14:50
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the Coraza Web Application Firewall (WAF) middleware for Fiber v3. The implementation includes a core Engine for managing WAF instances, support for manual reloads, and built-in metrics collection. Review feedback identifies a potential issue where an error from the request body reader is ignored, which could lead to downstream handlers receiving an empty request body. Additionally, a redundant call to the request body processing function was noted for removal to improve code clarity.

req.Body = io.NopCloser(io.MultiReader(rbr, req.Body))
}

return tx.ProcessRequestBody()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The tx.ReadRequestBodyFrom function, called earlier for accessible request bodies, already processes the body internally by calling tx.ProcessRequestBody. This final call to tx.ProcessRequestBody() is therefore redundant and could be confusing. It should be removed, and the function should return nil, nil to indicate that processing is complete without any interruptions.

Suggested change
return tx.ProcessRequestBody()
return nil, nil

@gaby gaby changed the title Add Coraza middleware for Fiber v3 feat: Add Coraza middleware Mar 28, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new Coraza WAF middleware for Fiber v3: documentation and CI badge, a new v3/coraza package implementing an Engine, middleware constructors, lifecycle (Init/Reload) and observability APIs, a metrics collector and snapshots, plus extensive tests and examples.

Changes

Cohort / File(s) Summary
Docs
v3/README.md, v3/coraza/README.md
Added Coraza entry and CI badge to v3/README.md; added comprehensive v3/coraza/README.md documenting install, API (coraza.New, coraza.NewEngine), config options, examples, lifecycle, and observability.
Engine & Middleware
v3/coraza/coraza.go
New Coraza package: Engine type, Config/MiddlewareConfig, New/NewEngine/Middleware/Reload/Init methods, request conversion, transaction creation, header/body inspection, interruption handling, block/error handler hooks, panic recovery, and logging/metrics integration.
Metrics
v3/coraza/metrics.go
Added MetricsCollector interface, MetricsSnapshot/EngineSnapshot/MetricsReport types, default in-memory collector implementation, and Engine methods MetricsSnapshot, Snapshot, and Report.
Tests
v3/coraza/coraza_test.go
Comprehensive tests for engine initialization (including RootFS), middleware runtime behavior (header/body blocking), custom Next/BlockHandler/ErrorHandler, metrics, engine isolation, reload semantics and failure, snapshot/reporting, and panic-recovery paths.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Fiber as Fiber Middleware
    participant Engine as Coraza Engine
    participant WAF as Coraza WAF / Transaction
    participant Handler as Block/Error Handler

    Client->>Fiber: HTTP request
    Fiber->>Engine: Check Next() (bypass?)
    alt Bypass
        Fiber->>Fiber: Record request metrics
        Fiber-->>Client: Forward request
    else Inspect
        Fiber->>Engine: Snapshot WAF / create transaction
        Engine->>WAF: Create transaction (with/without options)
        Fiber->>WAF: Process connection & headers
        alt Header interruption
            WAF-->>Fiber: Interruption
            Fiber->>Handler: Invoke BlockHandler/ErrorHandler
            Handler-->>Client: Block/Error response
        else Headers OK
            Fiber->>WAF: Optionally read & process body
            alt Body interruption
                WAF-->>Fiber: Interruption
                Fiber->>Handler: Invoke BlockHandler
                Handler-->>Client: Block response
            else No interruption
                Fiber->>Fiber: Record latency/metrics
                Fiber-->>Client: Forward request
            end
        end
    end
    Fiber->>Engine: Close transaction / update metrics
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • gaby
  • sixcolors
  • ReneWerner87
  • efectn

Poem

🐰 I hopped through rules and logs with glee,
Engines humming, metrics counting me,
Headers checked and bodies scanned,
Tests ensure the fences stand,
A rabbit thrives — secure and free!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title mentions 'Add Coraza middleware' which directly reflects the main change—introducing a new coraza middleware module for Fiber v3. However, it includes an emoji (🔥) which is considered noise per guidelines.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@v3/coraza/coraza.go`:
- Around line 56-62: The code currently enables matched-rule logging by default
via ConfigDefault (EnableErrorLog: true) and writes attacker-controlled data
using rule.URI() and rule.ErrorLog(); change this so matched-rule details are
either opt-in or redacted: add a distinct config flag (e.g.,
EnableMatchedRuleLogging or keep EnableErrorLog but default it to false in
ConfigDefault) and update the logging site(s) that call rule.URI() and
rule.ErrorLog() to check that flag before emitting raw values; if the flag is
disabled, log only a safe summary/identifier (e.g., rule.ID or masked text) or
redact/escape sensitive fragments prior to logging. Ensure references to
ConfigDefault, EnableErrorLog (or new EnableMatchedRuleLogging), and the logging
call sites that use rule.URI()/rule.ErrorLog() are updated accordingly.
- Around line 186-188: The code currently only applies cfg.BlockMessage when
non-empty so callers cannot reset to the package default; update both places
that set e.blockMessage (the Init function and the config update path that
references cfg.BlockMessage) to assign e.blockMessage = cfg.BlockMessage when
cfg.BlockMessage != "" else set e.blockMessage = defaultBlockMessage so an
explicit empty BlockMessage restores the defaultBlockMessage instead of
preserving the previous custom text. Ensure you update the assignments that
reference cfg.BlockMessage and e.blockMessage (including the Init method and the
config update logic) accordingly.
- Around line 386-395: The log reads e.reloadCount after releasing e.mu which
can race with another Reload(); fix by reading and storing e.reloadCount (and
any other fields needed for the log) while holding the lock in the Reload
implementation (use e.mu, e.reloadCount), then release e.mu and pass the local
variable into slog.Info (instead of accessing e.reloadCount after unlock) so the
logged value is consistent and race-free.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 226936d6-c7b3-415c-a90b-8ee19d4fcd2d

📥 Commits

Reviewing files that changed from the base of the PR and between 71ade1d and 79bf658.

⛔ Files ignored due to path filters (4)
  • .github/workflows/test-coraza.yml is excluded by !**/*.yml
  • go.work is excluded by !**/*.work, !**/*.work
  • v3/coraza/go.mod is excluded by !**/*.mod
  • v3/coraza/go.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (5)
  • v3/README.md
  • v3/coraza/README.md
  • v3/coraza/coraza.go
  • v3/coraza/coraza_test.go
  • v3/coraza/metrics.go
📜 Review details
🧰 Additional context used
🪛 LanguageTool
v3/coraza/README.md

[grammar] ~5-~5: Ensure spelling is correct
Context: --- id: coraza --- # Coraza Release [![Discord](https://img.shields.io/disco...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (6)
v3/coraza/metrics.go (1)

81-120: LGTM: the metrics collector keeps the hot path lean.

Atomics for request/block counters and a mutex only around latency aggregation are a good fit for middleware metrics, and the snapshot/report helpers return copy-style data instead of leaking mutable state.

Also applies to: 132-147

v3/coraza/coraza.go (2)

277-363: LGTM: the transaction wrapper fails closed.

Recovering around both request inspection and transaction cleanup, then always calling ProcessLogging() and Close(), makes the middleware much safer under panic/error paths.


541-548: No issue found — fiber.Ctx.Port() correctly returns the remote client port.

In Fiber v3.1.0, fiber.Ctx.Port() returns the remote (client) port, not the server port. The code at line 547 correctly populates RemoteAddr with the client IP and client port, which is what tx.ProcessConnection expects.

v3/coraza/coraza_test.go (1)

331-431: Nice coverage on lifecycle and fail-closed behavior.

The reload, failed re-init, and WAF panic tests hit the sharpest edges in the new Engine API and give good protection against regressions there.

v3/README.md (1)

24-24: LGTM: the index entry matches the new middleware and workflow.

v3/coraza/README.md (1)

33-123: Nice README coverage for the public surface.

The doc covers the config fields, New vs NewEngine, and the startup behavior around missing rule files, which are the key integration details for callers.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
v3/coraza/coraza.go (2)

361-362: Consider logging tx.Close() errors.

The error from tx.Close() is silently discarded. While this avoids masking the primary error, failed cleanup could indicate resource leaks worth observing.

♻️ Optional: log close errors
 	tx.ProcessLogging()
-	_ = tx.Close()
+	if err := tx.Close(); err != nil {
+		slog.Debug("[Coraza] transaction close failed", "error", err)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/coraza/coraza.go` around lines 361 - 362, The code currently discards the
error from tx.Close() after calling tx.ProcessLogging(); change it to capture
and log the Close error (e.g., err := tx.Close(); if err != nil { /* log at
warn/debug level */ }) so failed cleanup is observable without masking any
primary error; locate the calls to tx.ProcessLogging() and tx.Close() in
coraza.go and add a non-fatal log (using the existing logger in scope) when
tx.Close() returns an error.

514-528: Body reassignment may be unnecessary in Fiber context.

Line 527 reconstructs req.Body for potential re-reading, following standard Coraza connector patterns. However, downstream Fiber handlers use the original fiber.Ctx, not this converted *http.Request. The body reassignment has no effect on Fiber's request handling.

If Fiber's body is needed downstream, consider whether this code path serves a purpose or can be simplified.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/coraza/coraza.go` around lines 514 - 528, The reassignment of req.Body
after calling tx.ReadRequestBodyFrom and tx.RequestBodyReader (in coraza.go
within the code handling tx.IsRequestBodyAccessible()) is unnecessary for Fiber
because downstream handlers use fiber.Ctx rather than the converted
*http.Request; remove or guard the io.NopCloser(io.MultiReader(rbr, req.Body))
assignment to avoid a no-op and potential confusion. Specifically, in the block
that calls tx.ReadRequestBodyFrom(...) and tx.RequestBodyReader(...), either
delete the req.Body = ... line or add a runtime check for Fiber context (or
exported flag) and only reassign req.Body when running under a standard net/http
handler, leaving Fiber’s body untouched. Ensure references to
tx.ReadRequestBodyFrom and tx.RequestBodyReader remain and keep error handling
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@v3/coraza/coraza.go`:
- Around line 361-362: The code currently discards the error from tx.Close()
after calling tx.ProcessLogging(); change it to capture and log the Close error
(e.g., err := tx.Close(); if err != nil { /* log at warn/debug level */ }) so
failed cleanup is observable without masking any primary error; locate the calls
to tx.ProcessLogging() and tx.Close() in coraza.go and add a non-fatal log
(using the existing logger in scope) when tx.Close() returns an error.
- Around line 514-528: The reassignment of req.Body after calling
tx.ReadRequestBodyFrom and tx.RequestBodyReader (in coraza.go within the code
handling tx.IsRequestBodyAccessible()) is unnecessary for Fiber because
downstream handlers use fiber.Ctx rather than the converted *http.Request;
remove or guard the io.NopCloser(io.MultiReader(rbr, req.Body)) assignment to
avoid a no-op and potential confusion. Specifically, in the block that calls
tx.ReadRequestBodyFrom(...) and tx.RequestBodyReader(...), either delete the
req.Body = ... line or add a runtime check for Fiber context (or exported flag)
and only reassign req.Body when running under a standard net/http handler,
leaving Fiber’s body untouched. Ensure references to tx.ReadRequestBodyFrom and
tx.RequestBodyReader remain and keep error handling unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7e54b268-92bb-4a95-b9de-b3b977fdc21f

📥 Commits

Reviewing files that changed from the base of the PR and between 79bf658 and d120d2f.

📒 Files selected for processing (1)
  • v3/coraza/coraza.go
📜 Review details
🔇 Additional comments (14)
v3/coraza/coraza.go (14)

1-23: LGTM!

Package declaration and imports are appropriate for the WAF middleware implementation.


56-62: Matched-rule logging enabled by default may expose sensitive data.

EnableErrorLog: true causes logError() (lines 478-486) to emit rule.URI() and rule.ErrorLog() which can contain attacker-controlled request paths, query strings, and body fragments. Consider defaulting to false or redacting sensitive portions before logging.
[duplicate_comment, raise_major_issue]


64-104: LGTM!

The middleware configuration types and handler signatures are well-structured with clear documentation.


106-127: LGTM!

Engine struct is well-organized with clear separation of WAF state, configuration, and observability fields under mutex protection.


129-158: LGTM!

The fail-fast panic in New() is documented and appropriate for startup configuration errors. NewEngine() provides an error-returning alternative for advanced use cases.


186-188: No path to restore the default block message.

When cfg.BlockMessage is empty, the previous e.blockMessage value persists. Once a custom message is set, callers cannot reset to defaultBlockMessage by passing an empty string.

Consider always assigning the value:

if cfg.BlockMessage != "" {
    e.blockMessage = cfg.BlockMessage
} else {
    e.blockMessage = defaultBlockMessage
}

[duplicate_comment, raise_minor_issue]


194-203: Same limitation as Init: empty string doesn't restore default.

Consider changing the guard to restore defaultBlockMessage when msg is empty instead of silently returning.
[duplicate_comment, raise_minor_issue]


205-275: LGTM!

The Middleware() implementation has proper lifecycle management, correct metrics recording calls, and comprehensive error handling with appropriate fallback to default handlers.


277-339: LGTM!

Solid defensive implementation with panic recovery, proper transaction lifecycle management via deferred cleanup, and clear error propagation paths.


386-397: Data race: e.reloadCount read after mutex release.

Line 395 accesses e.reloadCount after the mutex is unlocked on line 393. A concurrent Reload() can modify this field, causing a race condition.

🔒 Fix: capture value before unlocking
 	e.mu.Lock()
 	e.waf = newWAF
 	e.initErr = nil
 	e.setWAFOptionsStateLocked(newWAF)
 	e.reloadCount++
 	e.reloadSuccessCount++
 	e.lastLoadedAt = time.Now()
+	reloadCount := e.reloadCount
 	e.mu.Unlock()
 
-	slog.Info("[Coraza] reload completed successfully", "reload_count", e.reloadCount)
+	slog.Info("[Coraza] reload completed successfully", "reload_count", reloadCount)
 	return nil

[duplicate_comment, raise_critical_issue]


399-440: LGTM!

Helper methods have proper locking semantics, and setWAFOptionsStateLocked() clearly indicates its lock precondition through naming convention.


442-467: LGTM!

Proper defensive slice copying and consistent read lock usage for the observability snapshot.


469-476: LGTM!

Default handlers are straightforward with appropriate header signaling for blocked requests.


533-630: LGTM!

Helper functions are well-implemented with appropriate error handling, graceful fallbacks for edge cases (e.g., invalid remote addresses), and proper defensive copying in cloneConfig().

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
v3/coraza/coraza_test.go (1)

254-258: Consider checking errors from io.ReadAll in test assertions.

Several test cases discard the error from io.ReadAll with a blank identifier. While this is common in tests where the response body is expected to be valid, explicitly checking for errors would make tests more robust and easier to debug if something unexpected happens.

Example fix for one occurrence
 	firstResp := performRequest(t, firstApp, httptest.NewRequest(http.MethodGet, "/?attack=1", nil))
 	defer firstResp.Body.Close()
-	firstBody, _ := io.ReadAll(firstResp.Body)
+	firstBody, err := io.ReadAll(firstResp.Body)
+	if err != nil {
+		t.Fatalf("failed to read first response body: %v", err)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/coraza/coraza_test.go` around lines 254 - 258, The test currently ignores
errors returned by io.ReadAll when reading firstResp.Body and secondResp.Body;
update the reads to capture and assert no error (e.g., err := io.ReadAll(...);
require.NoError(t, err) or if err != nil { t.Fatalf(...) }) for both firstBody
and secondBody so failures are reported clearly; apply this change where
io.ReadAll is called for firstResp.Body and secondResp.Body in the test that
uses performRequest, and ensure you close bodies before/after asserting errors
as needed.
v3/coraza/coraza.go (1)

567-584: Wildcard paths skip validation, relying on Coraza to handle errors.

The validateDirectivesFile function skips validation for paths containing * (glob patterns). This is reasonable since the actual file resolution happens inside Coraza's WithDirectivesFromFile. However, if the glob matches no files, Coraza may silently load no rules rather than returning an error, which could lead to a WAF running with incomplete protection.

Consider documenting this behavior or logging a warning when glob patterns are used.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/coraza/coraza.go` around lines 567 - 584, The function
validateDirectivesFile currently returns early when path contains "*" which
skips validation; update validateDirectivesFile to emit a clear warning whenever
a glob pattern is used (e.g., "directives path contains wildcard, Coraza will
resolve files at runtime; no files matched could leave WAF unprotected") so
operators are notified — use the package's logger (the same logging facility
used elsewhere in this package) to log at warning level and keep the early
return behavior; reference the relationship to Coraza's WithDirectivesFromFile
in the warning text to make intent clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@v3/coraza/coraza_test.go`:
- Around line 254-258: The test currently ignores errors returned by io.ReadAll
when reading firstResp.Body and secondResp.Body; update the reads to capture and
assert no error (e.g., err := io.ReadAll(...); require.NoError(t, err) or if err
!= nil { t.Fatalf(...) }) for both firstBody and secondBody so failures are
reported clearly; apply this change where io.ReadAll is called for
firstResp.Body and secondResp.Body in the test that uses performRequest, and
ensure you close bodies before/after asserting errors as needed.

In `@v3/coraza/coraza.go`:
- Around line 567-584: The function validateDirectivesFile currently returns
early when path contains "*" which skips validation; update
validateDirectivesFile to emit a clear warning whenever a glob pattern is used
(e.g., "directives path contains wildcard, Coraza will resolve files at runtime;
no files matched could leave WAF unprotected") so operators are notified — use
the package's logger (the same logging facility used elsewhere in this package)
to log at warning level and keep the early return behavior; reference the
relationship to Coraza's WithDirectivesFromFile in the warning text to make
intent clear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 28e16fed-78cf-47a5-867d-350d95657e15

📥 Commits

Reviewing files that changed from the base of the PR and between d120d2f and e211999.

📒 Files selected for processing (3)
  • v3/coraza/README.md
  • v3/coraza/coraza.go
  • v3/coraza/coraza_test.go
📜 Review details
🧰 Additional context used
🪛 LanguageTool
v3/coraza/README.md

[grammar] ~5-~5: Ensure spelling is correct
Context: --- id: coraza --- # Coraza Release [![Discord](https://img.shields.io/disco...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (7)
v3/coraza/README.md (1)

1-125: LGTM!

The documentation is comprehensive, well-structured, and accurately reflects the middleware implementation. The config table, usage examples, and behavioral notes provide clear guidance for users.

v3/coraza/coraza_test.go (1)

1-551: Test suite provides comprehensive coverage.

The test suite thoroughly exercises the middleware's functionality including:

  • Initialization paths (with/without config, local files, RootFS)
  • Request blocking (headers, body inspection)
  • Custom handlers (Next, BlockHandler, ErrorHandler)
  • Metrics tracking
  • Lifecycle management (Reload, Snapshot, Report)
  • Panic recovery with fail-closed behavior
  • Instance isolation

The use of t.Helper(), proper cleanup with defer, and the fake WAF implementation for panic testing demonstrate good testing practices.

v3/coraza/coraza.go (5)

179-184: LGTM - Past review concerns addressed.

The resolveBlockMessage() function correctly handles empty block message inputs by returning defaultBlockMessage, which addresses the past review concern about allowing callers to restore the default block message.


386-392: LGTM - Race condition fix verified.

The reloadCount value is now captured while holding the lock (line 388) and used for logging after the lock is released (line 392). This correctly addresses the past review concern about the data race on e.reloadCount.


253-257: LGTM - Logging does not expose attacker-controlled data.

The interruption logging only includes structured identifiers (rule_id, action, status) rather than raw request data like rule.URI() or rule.ErrorLog(). This addresses the past review concern about matched-rule logging potentially exposing attacker-controlled data.


1-650: Overall implementation is well-designed and addresses past review feedback.

The middleware implementation demonstrates:

  • Proper mutex usage for thread-safe access to engine state
  • Fail-closed behavior with panic recovery
  • Clean separation between simple (New) and advanced (NewEngine) APIs
  • Graceful degradation when reinitialization fails
  • Comprehensive observability via metrics and snapshots

All previously raised concerns from past reviews have been addressed.


501-516: This concern is not applicable—the request body handling is intentional and correct. The req at line 292 is created via c.Request.Clone(c.Context()), which produces an independent copy of the request. When tx.ReadRequestBodyFrom(bodyReader) consumes the body at line 506, it only affects the cloned request, not the original Fiber context's request. Downstream handlers receive the original context c (via c.Next() at line 316), so c.Body() continues to work correctly and is unaffected by WAF processing.

@GoFurry
Copy link
Copy Markdown
Author

GoFurry commented Mar 28, 2026

Updated in the latest commit.

Changes made:

  • removed RequestBodyLimit and RequestBodyInMemoryLimit from the middleware config
  • use Fiber app BodyLimit during request body inspection
  • replaced DebugLogger and EnableErrorLog with LogLevel
  • switched logging from slog to fiberlog

Also updated the README and tests accordingly.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
v3/coraza/coraza_test.go (1)

252-257: Handle response-body read errors instead of discarding them.

io.ReadAll errors are ignored at Line 252, Line 256, Line 309, Line 335, Line 397, and Line 423. In tests, this can hide real failures.

♻️ Minimal fix pattern
- firstBody, _ := io.ReadAll(firstResp.Body)
+ firstBody, err := io.ReadAll(firstResp.Body)
+ if err != nil {
+ 	t.Fatalf("failed to read response body: %v", err)
+ }

Also applies to: 309-309, 335-335, 397-397, 423-423

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/coraza/coraza_test.go` around lines 252 - 257, The test currently ignores
errors returned by io.ReadAll when reading response bodies (e.g., the calls
assigning firstBody, secondBody in coraza_test.go after performRequest), which
can hide failures; update each io.ReadAll usage in this file to check the
returned error and fail the test on error (use t.Fatalf or t.Fatalf-like
assertion) before using the body, and ensure bodies are still closed with defer
resp.Body.Close() where applicable; search for io.ReadAll occurrences around
variables firstBody, secondBody and any other similar reads (lines noted in the
review) and add proper error handling for each.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@v3/coraza/coraza_test.go`:
- Around line 252-257: The test currently ignores errors returned by io.ReadAll
when reading response bodies (e.g., the calls assigning firstBody, secondBody in
coraza_test.go after performRequest), which can hide failures; update each
io.ReadAll usage in this file to check the returned error and fail the test on
error (use t.Fatalf or t.Fatalf-like assertion) before using the body, and
ensure bodies are still closed with defer resp.Body.Close() where applicable;
search for io.ReadAll occurrences around variables firstBody, secondBody and any
other similar reads (lines noted in the review) and add proper error handling
for each.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6f8f1772-d0bb-4afe-a0f6-b058c9cc441e

📥 Commits

Reviewing files that changed from the base of the PR and between e211999 and 2f2bc61.

📒 Files selected for processing (4)
  • v3/coraza/README.md
  • v3/coraza/coraza.go
  • v3/coraza/coraza_test.go
  • v3/coraza/metrics.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • v3/coraza/coraza.go
📜 Review details
🧰 Additional context used
🪛 LanguageTool
v3/coraza/README.md

[grammar] ~5-~5: Ensure spelling is correct
Context: --- id: coraza --- # Coraza Release [![Discord](https://img.shields.io/disco...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (3)
v3/coraza/metrics.go (1)

67-158: Thread-safe metrics/reporting implementation looks solid.

The collector synchronization model and the Engine snapshot/report methods are clean and consistent with the middleware observability behavior.

v3/coraza/README.md (1)

26-121: API and behavior documentation is aligned with implementation.

The documented signatures/config fields and lifecycle/observability notes match the code paths in v3/coraza/coraza.go and v3/coraza/metrics.go.

v3/coraza/coraza_test.go (1)

24-457: Great coverage across middleware behavior, lifecycle, and observability paths.

The suite exercises key happy-path and failure-path behavior, including panic recovery and snapshot/report semantics.

@GoFurry GoFurry requested a review from gaby March 29, 2026 05:58
@gaby gaby requested a review from Copilot March 29, 2026 11:49
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Fiber v3 middleware module under v3/coraza that integrates the Coraza WAF engine, including engine lifecycle management (init/reload), request inspection, and lightweight observability (metrics + snapshots), along with CI and documentation wiring consistent with other contrib/v3 middlewares.

Changes:

  • Introduces coraza.Engine with New(...) middleware entrypoint, NewEngine(...), Engine.Init(...), and Engine.Reload().
  • Adds in-memory request metrics + engine lifecycle snapshots (MetricsSnapshot, EngineSnapshot, Report()).
  • Adds a dedicated test workflow, module docs, and updates workspace/index to include the new module.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
v3/coraza/coraza.go Core middleware + engine lifecycle/inspection logic.
v3/coraza/metrics.go Default metrics collector + snapshot/report DTOs.
v3/coraza/coraza_test.go End-to-end tests for init, blocking, body inspection, reload/snapshots, and error paths.
v3/coraza/README.md Module documentation and usage examples.
v3/coraza/go.mod New module definition and dependencies.
v3/coraza/go.sum Dependency checksums for the new module.
.github/workflows/test-coraza.yml CI workflow to run tests for the new module.
v3/README.md Adds coraza to the v3 middleware index list.
go.work Adds ./v3/coraza to the workspace.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +426 to +430
func newEngine(collector MetricsCollector) *Engine {
if collector == nil {
collector = NewDefaultMetricsCollector()
}

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newEngine only checks collector == nil, which doesn’t catch a typed-nil MetricsCollector (e.g. (*defaultMetricsCollector)(nil) stored in the interface). In that case Engine.Middleware() will panic on metrics.RecordRequest() and Engine.MetricsSnapshot() can panic on collector.GetMetrics(). Consider treating typed-nil collectors as nil (similar to how other middleware handles typed-nil interfaces) and falling back to NewDefaultMetricsCollector() (or defensively nil-check in callers).

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +70
type defaultMetricsCollector struct {
totalRequests uint64
blockedRequests uint64

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultMetricsCollector uses atomic.AddUint64/LoadUint64 on uint64 struct fields. On 32-bit architectures this can trigger sync/atomic: misaligned 64-bit atomic operation unless the fields are 64-bit aligned. Using atomic.Uint64 (or otherwise ensuring 64-bit alignment) for these counters would avoid that portability/concurrency hazard.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
v3/coraza/coraza_test.go (1)

478-498: Add reload coverage for no-directive engine mode.

Current reload tests only cover directive-backed configs. Add a case where the engine is initialized without DirectivesFile and Reload() is expected to succeed, so default mode doesn’t regress.

🧪 Suggested test
+func TestReloadWithoutDirectivesSucceeds(t *testing.T) {
+	engine, err := NewEngine(Config{
+		RequestBodyAccess: true,
+	})
+	if err != nil {
+		t.Fatalf("failed to initialize engine without directives: %v", err)
+	}
+
+	if err := engine.Reload(); err != nil {
+		t.Fatalf("expected reload without directives to succeed, got: %v", err)
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@v3/coraza/coraza_test.go` around lines 478 - 498, Add a test case that
initializes the engine in "no-directive" mode (i.e., create the engine without
setting DirectivesFile or any directive-backed config), call engine.Reload(),
then take engine.Snapshot() and assert ReloadSuccessTotal, InitSuccessTotal and
ReloadCount are incremented as in TestEngineSnapshotTracksLifecycleCounters; use
the same methods engine.Reload() and engine.Snapshot() and the same snapshot
fields ReloadSuccessTotal, InitSuccessTotal, ReloadCount to validate the
behavior so the default/no-directive reload path is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@v3/coraza/coraza.go`:
- Around line 364-366: The Reload() method incorrectly returns an error when
cfg.DirectivesFile is empty even though no-directive mode is valid; remove the
early return that checks if len(cfg.DirectivesFile) == 0 so Reload() will
proceed in no-directive configurations (i.e., delete or bypass the conditional
that returns fmt.Errorf("no configuration available for reload")). Ensure
Reload() still validates other necessary reload sources if any, but do not treat
an empty cfg.DirectivesFile as an error condition.
- Around line 580-588: In validateDirectivesFile, when path contains a wildcard
currently you only warn and return nil; instead resolve the wildcard against the
provided root fs.FS (e.g., use fs.Glob or walk root and match the pattern) to
ensure at least one file matches, and if no matches are found return a non-nil
error (and keep the warning log) so a mistyped wildcard doesn't silently open
the WAF; reference validateDirectivesFile, root (fs.FS) and path to locate where
to implement the glob/match check and error return.

In `@v3/coraza/metrics.go`:
- Around line 89-95: RecordLatency may silently overflow m.latencySum (type
time.Duration) when adding duration; update
defaultMetricsCollector.RecordLatency to detect saturation before adding (for
example compute newSum := m.latencySum + duration and if newSum < m.latencySum
or newSum < 0 treat as overflow) and clamp m.latencySum to a safe maximum (e.g.,
time.Duration(math.MaxInt64) or a defined MaxLatency) instead of wrapping; keep
the latencyCount increment but ensure the guard uses the same mutex and
references the method name RecordLatency and fields latencySum and latencyCount
so callers and metrics consumers are not corrupted by wrapped durations.

---

Nitpick comments:
In `@v3/coraza/coraza_test.go`:
- Around line 478-498: Add a test case that initializes the engine in
"no-directive" mode (i.e., create the engine without setting DirectivesFile or
any directive-backed config), call engine.Reload(), then take engine.Snapshot()
and assert ReloadSuccessTotal, InitSuccessTotal and ReloadCount are incremented
as in TestEngineSnapshotTracksLifecycleCounters; use the same methods
engine.Reload() and engine.Snapshot() and the same snapshot fields
ReloadSuccessTotal, InitSuccessTotal, ReloadCount to validate the behavior so
the default/no-directive reload path is covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f3532a14-46c0-4a4e-a17b-c8282dafb376

📥 Commits

Reviewing files that changed from the base of the PR and between 2f2bc61 and dd9e51d.

📒 Files selected for processing (3)
  • v3/coraza/coraza.go
  • v3/coraza/coraza_test.go
  • v3/coraza/metrics.go

@gaby gaby changed the title feat: Add Coraza middleware 🔥 feat: Add Coraza middleware Mar 29, 2026
@GoFurry
Copy link
Copy Markdown
Author

GoFurry commented Mar 30, 2026

Added 1.26.x to the Coraza test matrix in the latest commit.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +125 to +130
func New(config ...Config) fiber.Handler {
cfg := ConfigDefault
if len(config) > 0 {
cfg = config[0]
}

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New(config ...Config) currently discards ConfigDefault whenever a config is provided (cfg = config[0]). This means omitted fields fall back to their zero values (e.g., RequestBodyAccess becomes false, and LogLevel can become LevelTrace), which contradicts the documented defaults and can unintentionally disable request body inspection.

Copilot uses AI. Check for mistakes.
Comment on lines +143 to +151
// NewEngine creates and initializes an Engine with the provided configuration.
func NewEngine(cfg Config) (*Engine, error) {
engine := newEngine(cfg.MetricsCollector)
if err := engine.Init(cfg); err != nil {
return nil, err
}

return engine, nil
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MetricsCollector is part of Config, but it is only applied once in NewEngine via newEngine(cfg.MetricsCollector). Subsequent calls to Engine.Init(cfg) ignore cfg.MetricsCollector, so reinitializing an engine cannot change the collector even though the API suggests it can. Consider either applying cfg.MetricsCollector inside Init (and updating stored config accordingly) or documenting/removing it from the Init-level configuration to avoid misleading behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +582 to +587
fiberlog.Warnw(
"Coraza directives path uses glob matching and is expanded before initialization",
"path", path,
"note", "all matching directives files will be loaded in sorted order",
)

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveDirectivesFiles logs glob-related warnings using fiberlog.Warnw(...) directly, which bypasses the middleware/engine LogLevel configuration used elsewhere (via logWithLevel). If LogLevel is intended to control lifecycle logging, consider routing this warning through the same log-level filtering (e.g., pass the configured level into resolveDirectivesFiles or log from the caller).

Suggested change
fiberlog.Warnw(
"Coraza directives path uses glob matching and is expanded before initialization",
"path", path,
"note", "all matching directives files will be loaded in sorted order",
)

Copilot uses AI. Check for mistakes.
If you want the defaults, start from `coraza.ConfigDefault` and override the fields you need.
By default, the middleware starts without external rule files. Set `DirectivesFile` to load your Coraza or CRS ruleset.
Request body size follows the Fiber app `BodyLimit`.
Wildcard entries in `DirectivesFile` are resolved by Coraza at runtime. If a wildcard matches no files, the WAF may start without the expected rules loaded.
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README states that wildcard entries in DirectivesFile are resolved by Coraza at runtime and may start with no rules if there are no matches, but the implementation expands globs before initialization and returns an error when no files match. Please update this section to reflect the actual behavior (pre-expansion + fail on zero matches), or adjust the code to match the documented behavior.

Suggested change
Wildcard entries in `DirectivesFile` are resolved by Coraza at runtime. If a wildcard matches no files, the WAF may start without the expected rules loaded.
Wildcard entries in `DirectivesFile` are expanded before Coraza initializes. If a wildcard matches no files, initialization fails with an error and the middleware does not start.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants