← WAF: proteção e operação de aplicações
09 / 12 · 60 MIN

Integração HTTP e corpos de pedido

Executa o wrapper HTTP Coraza e compara bloqueio, observação, preservação do corpo e limites de inspeção antes da chamada à aplicação.

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")
}
NA PRÁTICA

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

Leva esta ideia contigo

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.

Criar conta

Referência: Coraza HTTP request middleware source · DR WAF 2026-09; selected AWS WAF and OWASP CRS operational concepts