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.HTTPHandler to map Fiber's fasthttp context 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.Reader for directives, allowing rules to be compiled directly into your binary using //go:embed.
  • πŸ”Œ Plug-and-Play Consumers: Use any io.Writer as 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 -> Fiber v2 (explicit package)
  • github.com/DavidHoenisch/fiber-coraza/v3 -> Fiber v3

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, LoggerIgnoreAllowEvents is true, so audit logs are only written for blocked/interrupted transactions. Set it to false if 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

FieldTypeDescription
Directivesio.ReaderSource of the Seclang configuration (Rules).
BlockboolIf true, stops the request on intervention. If false, logs but allows traffic.
Consumerio.WriterDestination for audit logs. Defaults to os.Stdout.
Nextfunc(*fiber.Ctx) bool (v2) / func(fiber.Ctx) bool (v3)Filter to skip the middleware (e.g., for health check endpoints).
Callbackfunc(types.MatchedRule)Optional low-level callback for Coraza error events.
WAFcoraza.WAFOptional pre-configured WAF instance. If provided, Directives is ignored.
InspectBodyboolIf true, inspects the request body. Defaults to true.
FailClosedboolIf true, returns 500 on internal errors (safe). If false, allows request (bypass). Defaults to true.
LoggerIgnoreAllowEventsboolIf 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.
ClientIpFromHeaderboolIf true, Coraza REMOTE_ADDR evaluation and audit logs use the configured header for the client IP instead of c.IP(). Defaults to false.
ClientIpHeaderstringHeader 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

  1. Fork the repository on GitHub
  2. Clone your fork locally:
    git clone https://github.com/YOUR_USERNAME/fiber-coraza.git
    cd fiber-coraza
    
  3. 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 fmt and go vet before 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:

  1. Unit tests demonstrating the feature or fix works
  2. Test coverage for happy paths and error cases
  3. Integration tests if modifying middleware behavior
  4. 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 feature
  • fix: Bug fix
  • docs: Documentation changes
  • test: Adding or updating tests
  • refactor: Code refactoring
  • perf: Performance improvements
  • chore: 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

  1. Update documentation if you're changing behavior or adding features
  2. Add yourself to contributors if it's your first contribution
  3. Ensure all tests pass: go test ./...
  4. Create a pull request with a clear description of changes
  5. 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.