fix(waf): GET-Requests ohne Body wurden von allen Phase-2-Regeln nie geprüft — v1.3.18
Schwerwiegende WAF-Lücke: der SPOE-Agent rief tx.ProcessRequestBody() nur bei len(body)>0 auf. In Coraza wird die GESAMTE Phase 2 (SQLi 942xxx, XSS 941xxx, LFI/RCE — alle prüfen ARGS aus dem Query-String) aber erst von ProcessRequestBody() ausgewertet. Damit lief jeder GET-Request ohne Body komplett ungeprüft an den Injection-Regeln vorbei — der häufigste Web-Angriffsvektor (?id=1' OR 1=1, ?x=<script>) war blind. Nur Requests MIT Body (POST/PUT/PROPFIND-XML) wurden inspiziert. Fix: ProcessRequestBody() läuft jetzt IMMER (mit/ohne Body). Regressionstest TestPhase2RequiresProcessRequestBody nagelt die Semantik fest. Alle 9 aktiven WAF-Domains sind in Detection-Modus → der Fix erzeugt nur mehr (echte) Alerts, blockt nichts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -131,14 +131,22 @@ func (a *SPOEAgent) handle(ctx context.Context, w *encoding.ActionWriter, m *enc
|
|||||||
// liefert den Body via `option http-buffer-request` (bis tune.bufsize) —
|
// liefert den Body via `option http-buffer-request` (bis tune.bufsize) —
|
||||||
// größere Bodies werden zur Prüfung gekappt. Content-Type kam bereits
|
// größere Bodies werden zur Prüfung gekappt. Content-Type kam bereits
|
||||||
// über die Header, sodass Coraza urlencoded/multipart/json korrekt parst.
|
// über die Header, sodass Coraza urlencoded/multipart/json korrekt parst.
|
||||||
if interruption == nil && len(body) > 0 {
|
if interruption == nil {
|
||||||
if it, _, err := tx.WriteRequestBody(body); err != nil {
|
if len(body) > 0 {
|
||||||
slog.Warn("waf: WriteRequestBody", "error", err)
|
if it, _, err := tx.WriteRequestBody(body); err != nil {
|
||||||
} else if it != nil {
|
slog.Warn("waf: WriteRequestBody", "error", err)
|
||||||
interruption = it
|
} else if it != nil {
|
||||||
} else {
|
interruption = it
|
||||||
it, err := tx.ProcessRequestBody()
|
}
|
||||||
if err != nil {
|
}
|
||||||
|
// ProcessRequestBody MUSS immer laufen — auch ohne Body. In Coraza wird
|
||||||
|
// die GESAMTE Phase 2 (SQLi 942xxx, XSS 941xxx, die den Query-String/ARGS
|
||||||
|
// prüfen) erst hier ausgewertet. Wurde das an len(body)>0 gekoppelt,
|
||||||
|
// blieben GET-Requests ohne Body von allen Phase-2-Regeln ungeprüft →
|
||||||
|
// Query-String-Angriffe (?id=1' OR 1=1, ?x=<script>) liefen komplett
|
||||||
|
// durch. Der häufigste Web-Angriffsvektor war damit blind.
|
||||||
|
if interruption == nil {
|
||||||
|
if it, err := tx.ProcessRequestBody(); err != nil {
|
||||||
slog.Warn("waf: ProcessRequestBody", "error", err)
|
slog.Warn("waf: ProcessRequestBody", "error", err)
|
||||||
} else if it != nil {
|
} else if it != nil {
|
||||||
interruption = it
|
interruption = it
|
||||||
|
|||||||
50
internal/waf/spoe_phase2_test.go
Normal file
50
internal/waf/spoe_phase2_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package waf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/corazawaf/coraza/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestPhase2RequiresProcessRequestBody nagelt die Coraza-Semantik fest, die der
|
||||||
|
// SPOE-Bug verletzt hatte: Phase-2-Regeln (SQLi 942xxx, XSS 941xxx — sie prüfen
|
||||||
|
// ARGS aus dem Query-String) werden ERST von ProcessRequestBody() ausgewertet.
|
||||||
|
// Koppelt man ProcessRequestBody an len(body)>0, bleiben GET-Requests ohne Body
|
||||||
|
// von der gesamten Phase 2 ungeprüft. Dieser Test schlägt fehl, sollte jemand
|
||||||
|
// den Aufruf je wieder body-abhängig machen.
|
||||||
|
func TestPhase2RequiresProcessRequestBody(t *testing.T) {
|
||||||
|
waf, err := coraza.NewWAF(coraza.NewWAFConfig().WithDirectives(
|
||||||
|
"SecRuleEngine DetectionOnly\n" +
|
||||||
|
"SecRule ARGS \"@rx attackpattern\" \"id:1234,phase:2,log,msg:'phase2-arg'\"\n"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewWAF: %v", err)
|
||||||
|
}
|
||||||
|
tx := waf.NewTransaction()
|
||||||
|
defer func() { tx.ProcessLogging(); _ = tx.Close() }()
|
||||||
|
|
||||||
|
tx.ProcessConnection("1.2.3.4", 0, "", 0)
|
||||||
|
tx.ProcessURI("/?x=attackpattern", "GET", "HTTP/1.1") // GET, kein Body
|
||||||
|
tx.AddRequestHeader("Host", "test")
|
||||||
|
tx.ProcessRequestHeaders()
|
||||||
|
|
||||||
|
// Vor ProcessRequestBody darf die Phase-2-Regel noch NICHT gefeuert haben.
|
||||||
|
if n := len(tx.MatchedRules()); n != 0 {
|
||||||
|
t.Fatalf("vor ProcessRequestBody: %d Matches, erwarte 0", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tx.ProcessRequestBody(); err != nil {
|
||||||
|
t.Fatalf("ProcessRequestBody: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jetzt MUSS die Phase-2-Regel gegen den Query-Arg gefeuert haben.
|
||||||
|
found := false
|
||||||
|
for _, mr := range tx.MatchedRules() {
|
||||||
|
if mr.Rule().ID() == 1234 {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("Phase-2-Regel feuerte auch nach ProcessRequestBody nicht — " +
|
||||||
|
"GET-Query-Args würden ungeprüft bleiben (SPOE-Bug)")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user