Fiber-Coraza
May 28, 2026 Β· View on GitHub
A high-performance Coraza WAF middleware for Go Fiber.
fiber-coraza brings the power of the OWASP Core Rule Set (CRS) and the coraza
WAD to Fiber. It is designed to be developer-friendly, offering type-safe
configuration and powerful logging flexibility.
Features
- β‘ Native Performance: Bypasses
adaptor.HTTPHandlerto map Fiber'sfasthttpcontext directly to Coraza transactions. - π Hybrid Logging: Combines Fiber's fast request access with Coraza's rule logic to produce structured JSON logs with zero extra allocations.
- π¦ Portable Rules: Accepts generic
io.Readerfor directives, allowing rules to be compiled directly into your binary using//go:embed. - π Plug-and-Play Consumers: Use any
io.Writeras a log destinationβno complex plugin registration required. - π§Ή Allow-Event Filtering: Skip audit logs for allowed rule matches by default to keep WAF logs focused on blocks.
Installation
# Fiber v2 middleware (explicit versioned package)
go get github.com/DavidHoenisch/fiber-coraza/v2
# Fiber v3 middleware
go get github.com/DavidHoenisch/fiber-coraza/v3
Version Support
github.com/DavidHoenisch/fiber-coraza/v2-> Fiberv2(explicit package)github.com/DavidHoenisch/fiber-coraza/v3-> Fiberv3
Breaking Change
The root package github.com/DavidHoenisch/fiber-coraza no longer exports middleware APIs.
Use explicit versioned imports (/v2 or /v3).
Quick Start
Fiber v2
package main
import (
"strings"
"github.com/gofiber/fiber/v2"
"github.com/DavidHoenisch/fiber-coraza/v2"
)
func main() {
app := fiber.New()
// Basic configuration with a simple rule
app.Use(coraza.NewCoraza(coraza.Config{
// Directives can be a string reader, a file, or an embedded FS
Directives: strings.NewReader(`
SecRuleEngine On
SecRule REMOTE_ADDR "@rx .*" "id:1,phase:1,deny,status:403"
`),
Block: true,
}))
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, Secure World!")
})
app.Listen(":3000")
}
Fiber v3
package main
import (
"strings"
coraza "github.com/DavidHoenisch/fiber-coraza/v3"
"github.com/gofiber/fiber/v3"
)
func main() {
app := fiber.New()
app.Use(coraza.NewCoraza(coraza.Config{
Directives: strings.NewReader(`
SecRuleEngine On
SecRule REMOTE_ADDR "@rx .*" "id:1,phase:1,deny,status:403"
`),
Block: true,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, Secure World!")
})
app.Listen(":3000")
}
Design Philosophy
1. The io.Reader Advantage (Embedding Rules)
fiber-coraza accepts an io.Reader for its directives.
This allows you to use Go's embed package to compile your WAF rules inside your binary, or, pass in the file from disk.
Example: Embedding OWASP CRS
package main
import (
"embed"
"io/fs"
"github.com/DavidHoenisch/fiber-coraza/v2"
)
//go:embed coraza.conf crs-setup.conf rules/*.conf
var rulesFS embed.FS
func main() {
// Create a combined reader or open a specific file from the embedded FS
f, _ := rulesFS.Open("coraza.conf")
app.Use(coraza.NewCoraza(coraza.Config{
Directives: f, // Fiber-Coraza reads directly from the binary
}))
}
2. Hybrid Logging & Consumers
Standard Coraza logging requires registering global plugins, which makes unit testing and custom integrations difficult.
fiber-coraza introduces the Consumer pattern. A Consumer is simply any
struct that implements io.Writer.
- Structured Data: The middleware automatically constructs a clean JSON object containing the timestamp, client IP, method, and matched rules.
- Hybrid Approach: It pulls request data (IP, URI) directly from Fiber (fast) and security decision data from Coraza.
- JSON Output: The data sent to your consumer looks like this:
- Allow-event filtering: By default,
LoggerIgnoreAllowEventsistrue, so audit logs are only written for blocked/interrupted transactions. Set it tofalseif you also want JSON audit logs for matched rules that ultimately allow the request.
{
"timestamp": "2023-10-27T10:00:00Z",
"client_ip": "192.168.1.50",
"request_uri": "/login",
"action": "Block",
"matched_rules": [
{ "id": 941100, "message": "XSS Attack Detected", "data": "<script>" }
]
}
To include audit logs for allowed requests that matched pass/log rules, disable allow-event filtering:
app.Use(coraza.NewCoraza(coraza.Config{
Consumer: os.Stdout,
LoggerIgnoreAllowEvents: false,
Directives: strings.NewReader(`
SecRuleEngine On
SecRule ARGS:id "@streq safe" "id:10,phase:1,pass,log,msg:'safe request matched'"
`),
}))
Configuration
| Field | Type | Description |
|---|---|---|
Directives | io.Reader | Source of the Seclang configuration (Rules). |
Block | bool | If true, stops the request on intervention. If false, logs but allows traffic. |
Consumer | io.Writer | Destination for audit logs. Defaults to os.Stdout. |
Next | func(*fiber.Ctx) bool (v2) / func(fiber.Ctx) bool (v3) | Filter to skip the middleware (e.g., for health check endpoints). |
Callback | func(types.MatchedRule) | Optional low-level callback for Coraza error events. |
WAF | coraza.WAF | Optional pre-configured WAF instance. If provided, Directives is ignored. |
InspectBody | bool | If true, inspects the request body. Defaults to true. |
FailClosed | bool | If true, returns 500 on internal errors (safe). If false, allows request (bypass). Defaults to true. |
LoggerIgnoreAllowEvents | bool | If true, suppresses audit logs for matched rules that allow the request. Blocked/interrupted transactions are still logged. Defaults to true; set to false to log allowed matches. |
ClientIpFromHeader | bool | If true, Coraza REMOTE_ADDR evaluation and audit logs use the configured header for the client IP instead of c.IP(). Defaults to false. |
ClientIpHeader | string | Header name to read the client IP from when ClientIpFromHeader is true. Defaults to X-Forwarded-By. Useful when the app sits behind a load balancer or reverse proxy. |
Essential WAF Directives
To get the most out of Coraza, you need to configure essential directives in your rule set. Here are the critical settings:
Request Body Inspection (Forms & JSON)
Why it's critical: Without these directives, Coraza won't inspect POST bodies, leaving a massive security gap for attackers to exploit.
# Enable request body access - REQUIRED for POST/PUT/PATCH inspection
SecRequestBodyAccess On
# Maximum request body size (13MB default)
SecRequestBodyLimit 13107200
# In-memory limit before streaming to disk
SecRequestBodyInMemoryLimit 131072
# What to do when body exceeds limit
SecRequestBodyLimitAction Reject
Form Data Parsing
Automatic for URL-encoded forms: When Content-Type: application/x-www-form-urlencoded is detected, Coraza automatically parses form fields into the ARGS_POST variable.
# Example rule checking form data
SecRule ARGS_POST:username "@contains admin" \
"id:1001,phase:2,deny,status:403,msg:'Admin username detected'"
JSON Request Body Parsing
Enable JSON processing: Add this rule to parse JSON payloads and populate ARGS_POST with dot-notation keys:
# Enable JSON body processor for application/json
SecRule REQUEST_HEADERS:Content-Type "^application/json" \
"id:200001,phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
# Support for +json MIME types (e.g., application/vnd.api+json)
SecRule REQUEST_HEADERS:Content-Type "^application/[a-z0-9.-]+[+]json" \
"id:200006,phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
How JSON is parsed:
- Input:
{"user": {"id": 123, "role": "admin"}, "tags": ["a", "b"]} - Variables created:
ARGS_POST:json.user.id= "123"ARGS_POST:json.user.role= "admin"ARGS_POST:json.tags.0= "a"ARGS_POST:json.tags.1= "b"
XML Request Body Parsing
# Enable XML body processor for XML content types
SecRule REQUEST_HEADERS:Content-Type "^(?:application(?:/soap\+|/)|text/)xml" \
"id:200000,phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML"
Body Processing Error Handling
# Deny requests with body processing errors (prevents evasion)
SecRule REQBODY_ERROR "!@eq 0" \
"id:200002,phase:2,t:none,log,deny,status:400,\
msg:'Failed to parse request body',\
logdata:'%{reqbody_error_msg}',severity:2"
# Strict multipart validation
SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
"id:200003,phase:2,t:none,log,deny,status:400,\
msg:'Multipart request body failed strict validation'"
Rule Engine Modes
# DetectionOnly: Log attacks but don't block (testing mode)
SecRuleEngine DetectionOnly
# On: Actively block attacks (production mode)
SecRuleEngine On
# Off: Disable WAF completely
SecRuleEngine Off
Response Body Inspection (Optional)
# Enable response body inspection
SecResponseBodyAccess On
# Which response types to inspect
SecResponseBodyMimeType text/plain text/html text/xml application/json
# Response body size limit (512KB)
SecResponseBodyLimit 524288
# What to do when response exceeds limit
SecResponseBodyLimitAction ProcessPartial
Complete Minimal Configuration
Here's a production-ready baseline configuration:
app.Use(coraza.NewCoraza(coraza.Config{
Directives: strings.NewReader(`
# Rule engine
SecRuleEngine On
# Request body settings
SecRequestBodyAccess On
SecRequestBodyLimit 13107200
SecRequestBodyInMemoryLimit 131072
SecRequestBodyLimitAction Reject
# Enable JSON parsing
SecRule REQUEST_HEADERS:Content-Type "^application/json" \
"id:200001,phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
# Enable XML parsing
SecRule REQUEST_HEADERS:Content-Type "^(?:application(?:/soap\+|/)|text/)xml" \
"id:200000,phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML"
# Body processing error handling
SecRule REQBODY_ERROR "!@eq 0" \
"id:200002,phase:2,t:none,log,deny,status:400,\
msg:'Failed to parse request body'"
# Example security rules
SecRule ARGS "@rx (?i:(union.*select|select.*from|insert.*into))" \
"id:300001,phase:2,deny,status:403,msg:'SQL Injection Detected'"
SecRule ARGS "@rx (?i:(<script|javascript:|onerror=))" \
"id:300002,phase:2,deny,status:403,msg:'XSS Attack Detected'"
`),
Block: true,
InspectBody: true,
FailClosed: true,
}))
Using OWASP Core Rule Set (CRS)
For production deployments, use the OWASP Core Rule Set:
# Download CRS
wget https://github.com/coreruleset/coreruleset/archive/v4.0.0.tar.gz
tar -xzf v4.0.0.tar.gz
//go:embed coreruleset-4.0.0/crs-setup.conf.example
//go:embed coreruleset-4.0.0/rules/*.conf
var crsFS embed.FS
// Load CRS rules
setupFile, _ := crsFS.Open("coreruleset-4.0.0/crs-setup.conf.example")
// Combine with your custom rules and pass to Directives
Custom Consumer Examples
Because the Consumer is just an io.Writer, you can easily send logs to files,
external services, or test buffers.
1. File Logging
Write logs to a local file for rotation or later analysis.
file, _ := os.OpenFile("waf.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
app.Use(coraza.NewCoraza(coraza.Config{
Consumer: file, // Logs are now written to waf.log
// ...
}))
2. The "Slack" Consumer (Advanced)
Since Write receives a byte slice of JSON, you can unmarshal it and send
alerts to external systems like Slack, Discord, or Datadog.
type SlackAlerter struct {
WebhookURL string
}
// Implement io.Writer
func (s *SlackAlerter) Write(p []byte) (n int, err error) {
// 1. Parse the log entry to check severity
var logEntry coraza.AuditLog
json.Unmarshal(p, &logEntry)
// 2. Only alert on BLOCKS
if logEntry.Action == "Block" {
msg := fmt.Sprintf("π¨ WAF Blocked IP %s on %s", logEntry.ClientIP, logEntry.RequestURI)
// ... logic to POST msg to s.WebhookURL ...
}
return len(p), nil
}
// Usage
app.Use(coraza.NewCoraza(coraza.Config{
Consumer: &SlackAlerter{WebhookURL: "https://hooks.slack.com/..."},
}))
3. Unit Testing (Spy)
Use a bytes.Buffer to capture logs in your tests to assert that attacks are being detected.
func TestWAF(t *testing.T) {
logSpy := new(bytes.Buffer)
app.Use(coraza.NewCoraza(coraza.Config{
Consumer: logSpy,
// ...
}))
// ... make request ...
if !strings.Contains(logSpy.String(), "Block") {
t.Error("Expected attack to be blocked and logged")
}
}
Contributing
Whether you're fixing bugs, adding features, improving documentation, or writing tests, your help is appreciated.
Getting Started
- Fork the repository on GitHub
- Clone your fork locally:
git clone https://github.com/YOUR_USERNAME/fiber-coraza.git cd fiber-coraza - Create a feature branch:
git checkout -b feature/your-feature-name
Development Setup
# Install dependencies
go mod download
# Run tests
go test -v ./...
# Run tests with coverage
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
Contribution Guidelines
Code Quality
- Write tests for all new features and bug fixes
- Maintain test coverage - aim for >80% coverage on new code
- Follow Go conventions - run
go fmtandgo vetbefore committing - Keep it simple - avoid over-engineering; prefer clear, maintainable code
- Document public APIs - add godoc comments for exported functions and types
Testing Requirements
All pull requests must include:
- Unit tests demonstrating the feature or fix works
- Test coverage for happy paths and error cases
- Integration tests if modifying middleware behavior
- Security tests if touching WAF rule processing
Example test structure:
func TestNewFeature(t *testing.T) {
app := fiber.New()
app.Use(NewCoraza(Config{
Directives: strings.NewReader(`/* your rules */`),
Block: true,
}))
// Test safe request
req := httptest.NewRequest("GET", "/test", nil)
resp, err := app.Test(req)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, 200, resp.StatusCode)
}
Commit Messages
Follow conventional commits format:
type(scope): short description
Longer description if needed.
Fixes #issue-number
Types:
feat: New featurefix: Bug fixdocs: Documentation changestest: Adding or updating testsrefactor: Code refactoringperf: Performance improvementschore: Maintenance tasks
Examples:
feat(middleware): add support for response body inspection
fix(logging): prevent race condition in audit log writer
docs(readme): add JSON parsing configuration examples
test(body): add SQL injection detection tests
Pull Request Process
- Update documentation if you're changing behavior or adding features
- Add yourself to contributors if it's your first contribution
- Ensure all tests pass:
go test ./... - Create a pull request with a clear description of changes
- Respond to review feedback promptly
Areas We Need Help With
- π§ͺ More test coverage - especially edge cases and error scenarios
- π Documentation improvements - tutorials, examples, use cases
- π Security testing - fuzzing, penetration testing, security audits
- π Performance optimization - benchmarking and profiling
- π OWASP CRS integration - better examples and helpers
- π Bug fixes - check our issues
Reporting Issues
When reporting bugs, please include:
- Go version (
go version) - Fiber version
- Coraza version
- Minimal reproduction code
- Expected vs actual behavior
- Any relevant logs or error messages
Questions or Ideas?
- π¬ Open a discussion for questions or feature proposals
- π Open an issue for bug reports
- π‘ Open a PR for contributions
License
MIT
Development Testing
Run unit tests:
go test ./...
Run fuzz tests:
make fuzz FUZZTIME=10s
Run mutation tests with the avito-tech fork of go-mutesting:
go install github.com/avito-tech/go-mutesting/cmd/go-mutesting@latest
make mutation
Mutation testing uses .go-mutesting.yml and .go-mutesting-blacklist for equivalent/unobservable mutants. You can target one package with make mutation-v2 or make mutation-v3, and override the per-mutant timeout with MUTATION_TIMEOUT=120.