Files
member-console/internal/middleware/tests/cross_origin_test.go
T
cgalo5758 0b28a9dc29 Remediate security audit findings
- Replace gorilla/csrf with net/http CrossOriginProtection
- Require valkey-password and add TLS options for session store
- End session at /logout and revoke refresh tokens
- Re-derive identity and roles from provider every five minutes
- Process each Stripe webhook event in its own Temporal workflow
- Give each outbox entry its own workflow with Temporal retries
- Guard against stale Stripe events with provider timestamps
- Derive transport security from base-url scheme
2026-09-09 13:25:43 -05:00

91 lines
3.5 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package tests
import (
"net/http"
"net/http/httptest"
"testing"
"git.coopcloud.tech/wiki-cafe/member-console/internal/middleware"
)
func protect(t *testing.T, cfg middleware.CSRFConfig) http.Handler {
t.Helper()
mw, err := middleware.CSRF(cfg)
if err != nil {
t.Fatalf("building protection: %v", err)
}
return mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
}
func post(h http.Handler, path string, headers map[string]string) int {
req := httptest.NewRequest(http.MethodPost, path, nil)
for k, v := range headers {
req.Header.Set(k, v)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec.Code
}
// A same-origin form post must pass. This is the case that matters most: the
// app sets Referrer-Policy: no-referrer, which used to null the Origin header
// and break native posts under gorilla/csrf. Sec-Fetch-Site is unaffected by
// referrer policy, so the old HTMX header workaround is no longer needed.
func TestSameOriginPostIsAllowed(t *testing.T) {
h := protect(t, middleware.CSRFConfig{})
if got := post(h, "/anything", map[string]string{"Sec-Fetch-Site": "same-origin"}); got != http.StatusOK {
t.Errorf("same-origin post got %d, want 200", got)
}
}
// The whole point of the migration.
func TestCrossSitePostIsRefused(t *testing.T) {
h := protect(t, middleware.CSRFConfig{})
if got := post(h, "/anything", map[string]string{"Sec-Fetch-Site": "cross-site"}); got != http.StatusForbidden {
t.Errorf("cross-site post got %d, want 403", got)
}
}
// A trusted origin is matched WITH its scheme. Under gorilla/csrf v1.7.3 the
// comparison was host-only, so http://... satisfied a policy written for
// https://... — that is GO-2025-3884 / CVE-2025-47909, and this pins the fix.
func TestTrustedOriginIsSchemeSensitive(t *testing.T) {
h := protect(t, middleware.CSRFConfig{TrustedOrigins: []string{"https://console.example.test"}})
if got := post(h, "/anything", map[string]string{
"Sec-Fetch-Site": "cross-site", "Origin": "https://console.example.test",
}); got != http.StatusOK {
t.Errorf("the trusted https origin got %d, want 200", got)
}
if got := post(h, "/anything", map[string]string{
"Sec-Fetch-Site": "cross-site", "Origin": "http://console.example.test",
}); got != http.StatusForbidden {
t.Errorf("plain http on a trusted https origin got %d, want 403 — this is the CVE", got)
}
}
// An origin without a scheme is a configuration error, not a silent no-op: a
// trusted origin that failed to register would leave a deployment rejecting
// its own posts.
func TestOriginWithoutSchemeIsRejectedAtConstruction(t *testing.T) {
if _, err := middleware.CSRF(middleware.CSRFConfig{TrustedOrigins: []string{"console.example.test"}}); err == nil {
t.Error("a scheme-less trusted origin was accepted; it must be a configuration error")
}
}
// Webhook paths authenticate by provider signature and must bypass entirely.
func TestBypassPatternExemptsAWebhookPath(t *testing.T) {
h := protect(t, middleware.CSRFConfig{BypassPatterns: []string{"/webhooks/stripe"}})
if got := post(h, "/webhooks/stripe", map[string]string{"Sec-Fetch-Site": "cross-site"}); got != http.StatusOK {
t.Errorf("exempt webhook path got %d, want 200", got)
}
if got := post(h, "/not-exempt", map[string]string{"Sec-Fetch-Site": "cross-site"}); got != http.StatusForbidden {
t.Errorf("the exemption leaked to another path: got %d, want 403", got)
}
}