Files
OwnCord/Server/api/waf_test.go
T
J3vbandClaude Fable 5 8579cb5d91 fix: batch of 25 correctness fixes across server and client (#1370)
* chore(workflows): raise subagent effort tiers (sonnet/haiku to xhigh, prove opus to high)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(voice): 6 defect(s) (OC-0098, OC-0004, OC-0005, OC-0006, OC-0007, OC-0020)

* fix(db): 1 defect(s) (OC-0096)

* fix(admin): 1 defect(s) (OC-0097)

* fix(auth): 2 defect(s) (OC-0099, OC-0021)

* fix(voice): 1 defect(s) (OC-0018)

* fix(admin): 1 defect(s) (OC-0045)

* fix(api): 1 defect(s) (OC-0103)

* fix(client): 1 defect(s) (OC-0105)

* fix(client): 1 defect(s) (OC-0107)

* fix(api): 1 defect(s) (OC-0109)

* fix(api): 1 defect(s) (OC-0112)

* test(admin): compare restore bytes with bytes.Equal

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(voice): 2 defect(s) (OC-0095, OC-0014)

OC-0095: createRoom never called setE2EEEnabled(true), so the full ECDH/HKDF/AES-GCM key exchange completed but frames still reached the SFU in plaintext.

OC-0014: token refresh timer was 23h while the server mints LiveKit tokens with a 5-minute TTL, so any reconnect after minute 5 presented an expired token.

* fix(profile): 2 defect(s) (OC-0100, OC-0102)

* fix(service): 1 defect(s) (OC-0022)

Archived channels were only read-only for SendMessage/DeleteMessage. Edit, reaction, pin and purge sinks bypassed the check. Route every write sink through a shared requireChannelWritable gate.

* fix(api): 1 defect(s) (OC-0048)

* chore(workflows): correct stale model labels in bughunt-fix phase details

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(client): 1 defect(s) (OC-0015)

* fix(voice): 1 defect(s) (OC-0002)

* test: fix two CI-only failures in the batch-4 test suite

The delete-account broadcast test now observes member_ban on a second
client's socket: the hub broadcasts and then force-disconnects the target,
so on a slow runner the close could beat the target's own copy of the
frame. The observer is also the party the event exists for.

The voice e2e mock now echoes the real joined channel id on voice_leave
(it hardcoded channel_id 0, which the dispatcher's channel-matched
self-leave teardown correctly ignores), and the rejoin test waits for the
mock's delayed echoes to settle before clicking the row again — clicking
inside the echo window toggled a leave instead of a join.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 14:49:27 +02:00

183 lines
5.8 KiB
Go

package api
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/corazawaf/coraza/v3/types"
)
func TestHandleWAFInterruption_WritesJSONAndStatus(t *testing.T) {
rr := httptest.NewRecorder()
handleWAFInterruption(rr, &types.Interruption{
Action: "deny",
Status: http.StatusForbidden,
RuleID: 942100,
Data: "SQL Injection detected",
})
if rr.Code != http.StatusForbidden {
t.Fatalf("status = %d, want 403", rr.Code)
}
if rr.Header().Get("Content-Type") != "application/json" {
t.Fatalf("Content-Type = %q, want application/json", rr.Header().Get("Content-Type"))
}
if strings.TrimSpace(rr.Body.String()) != `{"error":"request blocked by security rules"}` {
t.Fatalf("body = %q, want blocked JSON", rr.Body.String())
}
}
func TestWAFMiddleware_AllowsBenignRequest(t *testing.T) {
called := false
middleware := NewWAFMiddlewareCRS(2, CRSModeDetect)
handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodGet, "/api/v1/channels?q=hello", nil)
req.RemoteAddr = "127.0.0.1:9999"
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if !called {
t.Fatal("expected downstream handler to be called")
}
if rr.Code != http.StatusNoContent {
t.Fatalf("status = %d, want 204", rr.Code)
}
}
func TestWAFMiddleware_InvalidParanoiaLevelStillAllowsBenignRequest(t *testing.T) {
called := false
middleware := NewWAFMiddlewareCRS(99, CRSModeDetect)
handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodGet, "/api/v1/channels?q=hello", nil)
req.RemoteAddr = "127.0.0.1:9999"
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if !called {
t.Fatal("expected downstream handler to be called")
}
if rr.Code != http.StatusNoContent {
t.Fatalf("status = %d, want 204", rr.Code)
}
}
func TestWAFMiddleware_BlocksScannerUserAgent(t *testing.T) {
middleware := NewWAFMiddlewareCRS(2, CRSModeDetect)
handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("downstream handler should not be called for blocked scanner request")
}))
req := httptest.NewRequest(http.MethodGet, "/api/v1/channels", nil)
req.Header.Set("User-Agent", "sqlmap/1.8")
req.RemoteAddr = "127.0.0.1:9999"
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusForbidden {
t.Fatalf("status = %d, want 403; body = %s", rr.Code, rr.Body.String())
}
}
// Routes exempted from the app's global 1 MiB body cap (bodyCapExemptPrefixes
// in constants.go) must also be exempted from the inline WAF engine's own
// SecRequestBodyLimit, or coraza's default SecRequestBodyLimitAction (Reject)
// 413s the request as soon as its buffer hits 1 MiB — well below these
// routes' documented, larger caps.
func TestWAFMiddleware_AllowsLargePluginInstallBody(t *testing.T) {
requestBody := strings.Repeat("A", 2*1024*1024) // 2 MiB; within the 16 MiB plugin-install cap
middleware := NewWAFMiddlewareCRS(2, CRSModeDetect)
called := false
handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if len(body) != len(requestBody) {
t.Fatalf("body len = %d, want %d", len(body), len(requestBody))
}
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/plugins/install", strings.NewReader(requestBody))
req.Header.Set("Content-Type", "application/zip")
req.RemoteAddr = "127.0.0.1:9999"
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if !called {
t.Fatalf("expected downstream handler to be called, got status %d body %s", rr.Code, rr.Body.String())
}
if rr.Code != http.StatusNoContent {
t.Fatalf("status = %d, want 204; body = %s", rr.Code, rr.Body.String())
}
}
func TestWAFMiddleware_AllowsLargeAvatarUploadBody(t *testing.T) {
requestBody := strings.Repeat("A", 1_100_000) // >1 MiB; within the 2 MiB avatar cap
middleware := NewWAFMiddlewareCRS(2, CRSModeDetect)
called := false
handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if len(body) != len(requestBody) {
t.Fatalf("body len = %d, want %d", len(body), len(requestBody))
}
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodPost, "/api/v1/users/me/avatar", strings.NewReader(requestBody))
req.Header.Set("Content-Type", "image/png")
req.RemoteAddr = "127.0.0.1:9999"
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if !called {
t.Fatalf("expected downstream handler to be called, got status %d body %s", rr.Code, rr.Body.String())
}
if rr.Code != http.StatusNoContent {
t.Fatalf("status = %d, want 204; body = %s", rr.Code, rr.Body.String())
}
}
func TestWAFMiddleware_PreservesReadableBodyForDownstream(t *testing.T) {
const requestBody = `{"message":"hello world"}`
middleware := NewWAFMiddlewareCRS(2, CRSModeDetect)
handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if string(body) != requestBody {
t.Fatalf("body = %q, want %q", string(body), requestBody)
}
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodPost, "/api/v1/messages", strings.NewReader(requestBody))
req.Header.Set("Content-Type", "application/json")
req.RemoteAddr = "127.0.0.1:9999"
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusNoContent {
t.Fatalf("status = %d, want 204; body = %s", rr.Code, rr.Body.String())
}
}