← WAF: application protection and operations
09 / 12 · 60 MIN

HTTP integration and request bodies

Execute the Coraza HTTP wrapper and compare blocking, observation, body preservation and inspection limits before the application call.

Prepare the reproducible lab

In a new directory, save the code below as main.go. With Go installed, run go mod init example.test/waf-lab, then go get github.com/corazawaf/coraza/v3@v3.8.0 and go run. evidence.json. Recorded execution uses Go 1.27.1 and retains go.mod and go.sum. When present, acquisition.json adds compiler-acquisition evidence; it is not required for the exercise. After obtaining dependencies, you can run with GOPROXY=off. The program uses the official Coraza wrapper, httptest requests and ResponseRecorder. It opens no listener and establishes no TCP or TLS connection.

Connect a decision to HTTP flow

Earlier lessons observe transactions directly in the engine. Here, WrapHandler receives a request and decides whether to call the application handler. Original rule 5101 looks only for harmless marker DR-BLOCK in the comment field. In the blocking case, the response is 403, that rule matches and applicationCalls stays zero. Together these observations demonstrate interruption before the application in the tested wrapper. They do not establish that another proxy enforces the same decision. Nor do they measure attack detection: markers make the flow predictable.

Preserve legitimate request bodies

In the clean control, the handler reads comment=ordinary after inspection and the body read matches the body sent. This control matters because reading a stream can consume it. An integration can stop blocking yet still deliver empty data to the application. Compare what reaches the handler and the expected functional result instead of relying only on 200. In a fictional position import, an accepted file with no processed rows can be an integration regression. The correction should preserve the contract of supported clients.

Distinguish matching from blocking

In DetectionOnly, the same rule matches, but the handler executes and the client receives 200. An isolated match therefore does not identify the action applied to the client. Record effective mode, rule, phase and observed outcome. This distinction helps when the team prepares a move from observation to blocking: passing in observation does not prove the legitimate batch will pass in the next mode. Retain representative examples and acceptance criteria. Do not automatically translate this mode into AWS WAF Count; the scopes and APIs differ.

Test both sides of the limit

The teaching limit of 64 bytes enables comparison without large files. With Reject, a 96-byte body receives 413 before the application call. With ProcessPartial, a marker beyond the inspected portion does not match, but the application receives the complete body. Placed near the beginning, the same marker produces 403. The comparison separates delivered size from inspection coverage. Do not copy 64 bytes into production: characterize formats, sizes, buffering capacity and complete-content controls before choosing the contract for the service.

Diagnose the import and prepare RUN

In a fictional incident, the browser works while the batch fails only above a certain size. Reproduce with synthetic data, identify which component returns the error and check whether the handler executed. Compare effective configuration and the import contract before changing rules. If mitigation chooses partial inspection, document who validates the remaining portion and how the client recovers from rejection. RUN handover should include that matrix, owners and regression evidence. These original examples do not represent internal BNP Paribas procedures.

// Original DR HTTP integration fixture. Harmless markers and httptest only.
package main

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"runtime"
	"runtime/debug"
	"strings"
	"time"

	"github.com/corazawaf/coraza/v3"
	corazahttp "github.com/corazawaf/coraza/v3/http"
	"github.com/corazawaf/coraza/v3/types"
)

type Result struct {
	Name string `json:"name"`
	Config string `json:"configuration"`
	RequestBody string `json:"requestBody"`
	ApplicationBody string `json:"applicationBody"`
	ApplicationCalls int `json:"applicationCalls"`
	SyntheticEffects int `json:"syntheticEffects"`
	ProposedResponse string `json:"proposedResponse"`
	ResponseType string `json:"responseType"`
	Status int `json:"status"`
	ClientBody string `json:"clientBody"`
	ClientHeaders http.Header `json:"clientHeaders"`
	RuleIDs []int `json:"matchedRuleIds"`
}

func check(ok bool, message string) {
	if!ok {
		panic(message)
	}
}
func hashFile(name string) string {
	b, e:= os.ReadFile(name)
	if e!= nil {
		panic(e)
	}
	h:= sha256.Sum256(b)
	return hex.EncodeToString(h[:])
}
func evaluate(name, mode, options, rules, body, response, responseType string) Result {
	r:= Result{Name: name, RequestBody: body, ProposedResponse: response, ResponseType: responseType, RuleIDs: []int{}}
	r.Config = "SecRuleEngine " + mode + "\nSecRequestBodyAccess On\nSecResponseBodyAccess On\nSecResponseBodyMimeTypesClear\nSecResponseBodyMimeType text/plain\nSecAuditEngine Off\n" + options + "\n" + rules
	waf, err:= coraza.NewWAF(coraza.NewWAFConfig.WithDirectives(r.Config).WithErrorCallback(func(m types.MatchedRule) { r.RuleIDs = append(r.RuleIDs, m.Rule.ID) }))
	if err!= nil {
		panic(fmt.Sprintf("%s: %v", name, err))
	}
	app:= http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		data, err:= io.ReadAll(req.Body)
		if err!= nil {
			panic(err)
		}
		r.ApplicationBody = string(data)
		r.ApplicationCalls++
		// A counter only: no database, payment, durable write or external operation.
		r.SyntheticEffects++
		w.Header.Set("Content-Type", responseType)
		w.Header.Set("X-Lab-App", "called")
		_, err = io.WriteString(w, response)
		if err!= nil {
			panic(err)
		}
	})
	req:= httptest.NewRequest("POST", "http://training.example.test/import", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	recorder:= httptest.NewRecorder
	corazahttp.WrapHandler(waf, app).ServeHTTP(recorder, req)
	res:= recorder.Result
	defer res.Body.Close
	data, err:= io.ReadAll(res.Body)
	if err!= nil {
		panic(err)
	}
	r.Status = res.StatusCode
	r.ClientBody = string(data)
	r.ClientHeaders = res.Header.Clone
	return r
}
func has(r Result, id int) bool {
	for _, n:= range r.RuleIDs {
		if n == id {
			return true
		}
	}
	return false
}
func main {
	check(len(os.Args) == 2, "output path required")
	requestRule:= `SecRule ARGS:comment "@contains DR-BLOCK" "id:5101,phase:2,t:none,deny,status:403,log,msg:'original request marker'"`
	responseRule:= `SecRule RESPONSE_BODY "@contains DR-RESPONSE" "id:5201,phase:4,t:none,deny,status:403,log,msg:'original response marker'"`
	rules:= requestRule + "\n" + responseRule
	plain:= "text/plain"
	o:= map[string]Result{}
	o["cleanCopyback"] = evaluate("clean", "On", "", rules, "comment=ordinary", "accepted", plain)
	o["requestBlock"] = evaluate("request-marker", "On", "", rules, "comment=DR-BLOCK", "accepted", plain)
	o["requestDetection"] = evaluate("request-detection", "DetectionOnly", "", rules, "comment=DR-BLOCK", "accepted", plain)
	late:= "comment=" + strings.Repeat("x", 80) + "DR-BLOCK"
	early:= "comment=DR-BLOCK" + strings.Repeat("x", 80)
	o["requestLimitReject"] = evaluate("request-limit-reject", "On", "SecRequestBodyLimit 64\nSecRequestBodyLimitAction Reject", rules, late, "accepted", plain)
	o["partialBeyondLimit"] = evaluate("partial-late", "On", "SecRequestBodyLimit 64\nSecRequestBodyLimitAction ProcessPartial", rules, late, "accepted", plain)
	o["partialWithinLimit"] = evaluate("partial-early", "On", "SecRequestBodyLimit 64\nSecRequestBodyLimitAction ProcessPartial", rules, early, "accepted", plain)
	o["requestAccessOff"] = evaluate("request-body-off", "On", "SecRequestBodyAccess Off", rules, "comment=DR-BLOCK", "accepted", plain)
	o["responseBlock"] = evaluate("response-marker", "On", "", rules, "comment=ordinary", "DR-RESPONSE", plain)
	o["responseMimeExcluded"] = evaluate("response-mime-excluded", "On", "", rules, "comment=ordinary", "DR-RESPONSE", "application/octet-stream")
	o["responseAccessOff"] = evaluate("response-body-off", "On", "SecResponseBodyAccess Off", rules, "comment=ordinary", "DR-RESPONSE", plain)
	o["responseLimitReject"] = evaluate("response-limit-reject", "On", "SecResponseBodyLimit 64\nSecResponseBodyLimitAction Reject", rules, "comment=ordinary", strings.Repeat("r", 96), plain)
	o["responseDetection"] = evaluate("response-detection", "DetectionOnly", "", rules, "comment=ordinary", "DR-RESPONSE", plain)
	a:= o["cleanCopyback"]
	check(a.Status == 200 && a.ApplicationCalls == 1 && a.ApplicationBody == a.RequestBody, "clean copyback")
	a = o["requestBlock"]
	check(a.Status == 403 && a.ApplicationCalls == 0 && has(a, 5101), "request enforcement")
	a = o["requestDetection"]
	check(a.Status == 200 && a.ApplicationCalls == 1 && has(a, 5101) && a.ApplicationBody == a.RequestBody, "request detection")
	a = o["requestLimitReject"]
	check(a.Status == 413 && a.ApplicationCalls == 0, "request limit reject")
	a = o["partialBeyondLimit"]
	check(a.Status == 200 && a.ApplicationCalls == 1 &&!has(a, 5101) && a.ApplicationBody == a.RequestBody, "partial late marker and full copyback")
	a = o["partialWithinLimit"]
	check(a.Status == 403 && a.ApplicationCalls == 0 && has(a, 5101), "partial early marker")
	a = o["requestAccessOff"]
	check(a.Status == 200 && a.ApplicationCalls == 1 &&!has(a, 5101) && a.ApplicationBody == a.RequestBody, "request access off")
	a = o["responseBlock"]
	check(a.Status == 403 && a.ApplicationCalls == 1 && a.SyntheticEffects == 1 && has(a, 5201) && a.ClientBody == "" && a.ClientHeaders.Get("X-Lab-App") == "", "response block after effect")
	a = o["responseMimeExcluded"]
	check(a.Status == 200 && a.ClientBody == "DR-RESPONSE" &&!has(a, 5201), "response MIME boundary")
	a = o["responseAccessOff"]
	check(a.Status == 200 && a.ClientBody == "DR-RESPONSE" &&!has(a, 5201), "response body access off")
	a = o["responseLimitReject"]
	check(a.Status == 500 && a.ApplicationCalls == 1 && a.SyntheticEffects == 1 && a.ClientBody == "", "response limit after effect")
	a = o["responseDetection"]
	check(a.Status == 200 && a.ApplicationCalls == 1 && a.ClientBody == "DR-RESPONSE" && has(a, 5201), "response detection")
	info, ok:= debug.ReadBuildInfo
	check(ok, "build info")
	version:= ""
	for _, dep:= range info.Deps {
		if dep.Path == "github.com/corazawaf/coraza/v3" {
			version = dep.Version
		}
	}
	check(version == "v3.8.0", "pinned engine")
	artifacts:= map[string]string{}
	for _, name:= range []string{"main.go", "go.mod", "go.sum"} {
		artifacts[name] = hashFile(name)
	}
	if _, err:= os.Stat("acquisition.json"); err == nil {
		artifacts["acquisition.json"] = hashFile("acquisition.json")
	}
	evidence:= map[string]any{"checkedAt": time.Now.UTC.Format(time.RFC3339), "engine": "Coraza " + version, "goVersion": runtime.Version, "passed": 12, "failed": 0, "scope": "Actual Coraza WrapHandler with httptest requests and response recorders; no listener, TCP/TLS exchange, proxy deployment, CRS rules, AWS WAF, attack traffic, financial operation or production efficacy claim.", "observations": o, "artifacts": artifacts, "applicationEffectScope": "Original in-memory counter increments before producing a response; not a transactional system.", "independentSpecialistReview": false}
	data, err:= json.MarshalIndent(evidence, "", " ")
	if err!= nil {
		panic(err)
	}
	if err = os.WriteFile(os.Args[1], append(data, '\n'), 0600); err!= nil {
		panic(err)
	}
	fmt.Println("Coraza HTTP wrapper: twelve original observation groups passed")
}
IN PRACTICE

A fictional batch receives 413 and the handler is not called. Switching to ProcessPartial restores delivery but requires deciding how complete content is validated.

Common pitfalls

Interpreting a match as blocking, 200 as contract fulfillment or ProcessPartial as complete inspection; generalizing httptest to network, TLS or another connector.

Related topics: WAF architecture and coverage · Parsing and inspection limits · Logs, changes, and RUN handover

Take this idea with you

Validate what the engine observes, what the wrapper interrupts and what the application receives. All three forms of evidence are needed to accept integration.

Create account

Reference: Coraza HTTP request middleware source · BigSavant WAF 2026-09; selected AWS WAF and OWASP CRS operational concepts