mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* test(server): admin/handlers/channels — test-audit 2026-08-19 fixes * test(server): api/constants — test-audit 2026-08-19 fixes * test(server): api/middleware — test-audit 2026-08-19 fixes * test(server): api/waf — test-audit 2026-08-19 fixes * test(server): auth/totp/encrypt — test-audit 2026-08-19 fixes * test(server): db/session/expiry/test — test-audit 2026-08-19 fixes * test(server): migrations/030/attachments/unlink/on/message/delete — test-audit 2026-08-19 fixes * test(server): updater/download — test-audit 2026-08-19 fixes * test(server): ws/handlers_command — test-audit 2026-08-19 fixes * test(server): ws/hub/broadcast — test-audit 2026-08-19 fixes * test(server): ws/hub/events — test-audit 2026-08-19 fixes * test(server): ws/livekit/webhook — test-audit 2026-08-19 fixes * test(server): ws/voice/controls — test-audit 2026-08-19 fixes * test(server): ws/voice/join — test-audit 2026-08-19 fixes * test(server): ws/voice/moderation — test-audit 2026-08-19 fixes * test(rust): src-tauri/src/commands.rs — test-audit 2026-08-19 fixes * test(rust): src-tauri/src/secret_store.rs — test-audit 2026-08-19 fixes * test(rust): src-tauri/src/update_commands.rs — test-audit 2026-08-19 fixes * test(client): src/components/ChannelSidebar.ts — test-audit 2026-08-19 fixes * test(client): src/lib/ws.ts — test-audit 2026-08-19 fixes * test(rust): src-tauri/src/credentials.rs — test-audit 2026-08-19 fixes * test(rust): src-tauri/src/tofu.rs — test-audit 2026-08-19 fixes * test(client): src/lib/hostValidation.ts — test-audit 2026-08-19 fixes * test(client): src/lib/rate-limiter.ts — test-audit 2026-08-19 fixes * test(client): src/pages/connect-page/LoginForm.ts — test-audit 2026-08-19 fixes * test(client): src/pages/main-page/SidebarArea.ts — test-audit 2026-08-19 fixes * test(client): src/stores/voice.store.ts — test-audit 2026-08-19 fixes * test(client): tests/browser/smoke.test.ts — test-audit 2026-08-19 fixes * test(client): tests/unit/media.test.ts — test-audit 2026-08-19 fixes * test(client): tests/unit/renderers.test.ts — test-audit 2026-08-19 fixes * test(client): src/components/UserProfilePopup.ts — test-audit 2026-08-19 fixes * test(client): src/lib/e2eeCrypto.ts — test-audit 2026-08-19 fixes * test(client): tests/unit/log-persistence.test.ts — test-audit 2026-08-19 fixes * test(client): keep tests/browser out of the jsdom suite and run it in CI * test(server): ws/hub_broadcast_test.go — bytes.Equal payload compare (gocritic) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(client): src/lib/credentials.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/dispatcher.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/permissions.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/rate-limiter.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/hostValidation.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/stores/messages.store.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/e2eeCrypto.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/ws.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/identity.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/lib/livekitE2EE.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/stores/auth.store.ts — test-audit 2026-08-19 round 2 (Stryker) * test(client): src/stores/voice.store.ts — test-audit 2026-08-19 round 2 (Stryker) * docs: test audit 2026-08-19 — findings, fixes, measured baselines Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(graph): refresh the knowledge graph after the 2026-08-19 test audit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
245 lines
8.5 KiB
Go
245 lines
8.5 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"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())
|
|
}
|
|
}
|
|
|
|
// The inline engine's four phase-2 request-body rules must actually block a
|
|
// body-borne attack payload, not just log it: SQLi (942100), XSS (941100),
|
|
// path traversal (930100) and command injection (932100) are all
|
|
// deny,status:403 under an always-on SecRuleEngine. Every other blocking test
|
|
// in the suite fires the phase-1 User-Agent rule on a bodyless GET, so this is
|
|
// what pins wafInspectRequestBody's interruption path.
|
|
//
|
|
// CRS mode off is deliberate: with no CRS engine attached the inline engine is
|
|
// the only thing that can block, so the asserted rule id proves the inline
|
|
// rule fired. Detect mode is included because it is the production default and
|
|
// its CRS engine never interrupts (DetectionOnly), so the block must still
|
|
// come from the inline engine — block mode is left out precisely because there
|
|
// the CRS layer could be the one blocking.
|
|
//
|
|
// Payloads are form-urlencoded: that is the body form coraza parses into
|
|
// ARGS/REQUEST_BODY for this engine (it loads no coraza.conf-recommended, so
|
|
// no JSON body processor is selected — see waf_crs_test.go for the CRS layer,
|
|
// which does inspect JSON bodies).
|
|
func TestWAFMiddleware_BlocksAttackPayloadInRequestBody(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
body string
|
|
ruleID int
|
|
}{
|
|
{"sqli", `q=1%27%20OR%20%271%27%3D%271%20--%20`, 942100},
|
|
{"xss", `q=%3Cscript%3Ealert%281%29%3C%2Fscript%3E`, 941100},
|
|
{"path_traversal", `q=..%2F..%2Fetc%2Fpasswd`, 930100},
|
|
{"command_injection", `q=hello%20%7C%20id`, 932100},
|
|
}
|
|
|
|
for _, mode := range []string{CRSModeOff, CRSModeDetect} {
|
|
middleware := NewWAFMiddlewareCRS(2, mode)
|
|
for _, tc := range cases {
|
|
t.Run(mode+"/"+tc.name, func(t *testing.T) {
|
|
handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
t.Errorf("downstream handler must not be called for a %s request body", tc.name)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/messages", strings.NewReader(tc.body))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("User-Agent", "OwnCordClient/1.0")
|
|
req.RemoteAddr = "127.0.0.1:9999"
|
|
rr := httptest.NewRecorder()
|
|
|
|
out := captureSlog(t, func() { handler.ServeHTTP(rr, req) })
|
|
|
|
if rr.Code != http.StatusForbidden {
|
|
t.Fatalf("status = %d, want 403; body = %s", rr.Code, rr.Body.String())
|
|
}
|
|
if strings.TrimSpace(rr.Body.String()) != `{"error":"request blocked by security rules"}` {
|
|
t.Fatalf("body = %q, want blocked JSON", rr.Body.String())
|
|
}
|
|
if want := fmt.Sprintf("rule_id=%d", tc.ruleID); !strings.Contains(out, want) {
|
|
t.Fatalf("blocked by the wrong rule: want %s in\n%s", want, out)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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())
|
|
}
|
|
}
|