Preparar o laboratório reproduzível
Numa pasta nova, guarda o código abaixo como main.go. Com Go instalado, executa go mod init example.test/waf-lab, depois go get github.com/corazawaf/coraza/v3@v3.8.0 e go run . evidence.json. A execução registada usa Go 1.27.1 e preserva go.mod e go.sum. O ficheiro acquisition.json, quando presente, acrescenta evidência da aquisição do compilador; não é necessário para o exercício. Depois de obter dependências, podes executar com GOPROXY=off. O programa usa o wrapper oficial Coraza, pedidos httptest e ResponseRecorder. Não abre listener nem estabelece uma ligação TCP ou TLS.
Ligar uma decisão ao fluxo HTTP
As aulas anteriores observam transações diretamente no motor. Aqui, WrapHandler recebe um pedido e decide se chama o handler da aplicação. A regra original 5101 procura apenas o marcador inofensivo DR-BLOCK no campo comment. No caso de bloqueio, a resposta é 403, há correspondência dessa regra e applicationCalls fica a zero. Esta combinação demonstra a interrupção antes da aplicação no wrapper ensaiado. Não demonstra que um proxy diferente aplica a mesma decisão. Também não mede deteção de ataques: os marcadores servem para tornar o fluxo previsível.
Conservar o corpo dos pedidos legítimos
No controlo limpo, o handler lê comment=ordinary depois da inspeção e o corpo lido coincide com o enviado. Este controlo é importante porque ler um stream pode consumi-lo. Uma integração pode deixar de bloquear e, mesmo assim, entregar dados vazios à aplicação. Compara o conteúdo que chega ao handler e o resultado funcional esperado, sem depender apenas do 200. Numa importação fictícia de posições, um ficheiro aceite mas sem linhas processadas pode ser uma regressão de integração. A correção deve preservar o contrato dos clientes suportados.
Distinguir correspondência de bloqueio
Em DetectionOnly, a mesma regra corresponde, mas o handler executa e o cliente recebe 200. Por isso, uma correspondência isolada não identifica a ação aplicada ao cliente. Regista o modo efetivo, a regra, a fase e o resultado observado. Esta distinção ajuda quando a equipa prepara uma mudança de observação para bloqueio: passar em observação não prova que o batch legítimo irá passar no modo seguinte. Mantém exemplos representativos e critérios de aceitação. Não traduzas automaticamente este modo para Count de AWS WAF; os âmbitos e APIs são diferentes.
Testar os dois lados do limite
O limite didático de 64 bytes permite comparar comportamentos sem grandes ficheiros. Com Reject, um corpo de 96 bytes recebe 413 antes da chamada à aplicação. Com ProcessPartial, um marcador colocado depois da porção inspecionada não corresponde, mas a aplicação recebe o corpo completo. Colocado no início, o mesmo marcador provoca 403. A comparação separa tamanho entregue de cobertura de inspeção. Não copies o valor de 64 bytes para produção: caracteriza formatos, tamanhos, capacidade de buffering e controlos do conteúdo completo antes de escolher o contrato.
Diagnosticar o import e preparar RUN
Num incidente fictício, o browser funciona e o batch falha apenas acima de determinado tamanho. Reproduz com dados sintéticos, identifica quem devolve o erro e verifica se o handler chegou a executar. Compara configuração efetiva e contrato de importação antes de alterar regras. Se a mitigação escolher inspeção parcial, documenta quem valida a parte restante e como o cliente recupera de uma recusa. A passagem para RUN deve incluir essa matriz, responsáveis e regressão. Os exemplos são originais e não representam procedimentos internos do BNP Paribas.
// 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")
}
Um batch fictício recebe 413 e o handler não é chamado. Mudar para ProcessPartial recupera a entrega, mas exige decidir como validar o conteúdo completo.
Armadilhas comuns
Interpretar correspondência como bloqueio, 200 como contrato cumprido ou ProcessPartial como inspeção completa; generalizar httptest para rede, TLS ou outro conector.
Tópicos relacionados: Arquitetura e cobertura do WAF · Parsing e limites de inspeção · Logs, mudanças e passagem para RUN
Valida o que o motor observa, o que o wrapper interrompe e o que a aplicação recebe. Essas três evidências são necessárias para aceitar a integração.
Referência: Coraza HTTP request middleware source · DR WAF 2026-09; selected AWS WAF and OWASP CRS operational concepts