Files
web/websec/websec_test.go
gamertan a18f1dd22a
verify / verify (push) Successful in 3m1s
security: harden first preview boundaries
Sanitized snapshot of private source 13a965dd6ea705dd92499f7dbeaa00c25c15247d. Require same-origin evidence for unsafe methods, fail closed on invalid authentication middleware configuration, and bound untrusted request metadata.

AI-Assistance: OpenAI Codex assisted implementation, testing, and security review.
Signed-off-by: Cole Speelman <crspeelman@gmail.com>
2026-08-16 19:20:14 -04:00

107 lines
3.3 KiB
Go

// SPDX-License-Identifier: MPL-2.0
package websec
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestSameOriginRejectsCrossSiteAndContradiction(t *testing.T) {
request := httptest.NewRequest(http.MethodPost, "https://example.test/change", nil)
request.Header.Set("Origin", "https://example.test")
request.Header.Set("Sec-Fetch-Site", "same-origin")
if !SameOrigin(request, "https://example.test") {
t.Fatal("same origin rejected")
}
request.Header.Set("Sec-Fetch-Site", "cross-site")
if SameOrigin(request, "https://example.test") {
t.Fatal("cross-site accepted")
}
request.Header.Set("Sec-Fetch-Site", "same-origin")
request.Header.Set("Origin", "https://attacker.test")
if SameOrigin(request, "https://example.test") {
t.Fatal("foreign origin accepted")
}
}
func TestSameOriginRequiresEvidenceForUnsafeRequests(t *testing.T) {
request := httptest.NewRequest(http.MethodPost, "https://example.test/change", nil)
if SameOrigin(request, "https://example.test") {
t.Fatal("unsafe request without origin evidence accepted")
}
request.Header.Set("Sec-Fetch-Site", "same-origin")
if !SameOrigin(request, "https://example.test") {
t.Fatal("same-origin fetch metadata rejected")
}
request = httptest.NewRequest(http.MethodGet, "https://example.test/read", nil)
if !SameOrigin(request, "https://example.test") {
t.Fatal("safe request without browser metadata rejected")
}
}
func TestSameOriginRejectsMalformedConfiguredAndPresentedOrigins(t *testing.T) {
request := httptest.NewRequest(http.MethodPost, "https://example.test/change", nil)
request.Header.Set("Origin", "https://example.test")
for _, allowed := range []string{
"https://user@example.test",
"https://example.test?scope=wrong",
"https://example.test#wrong",
} {
if SameOrigin(request, allowed) {
t.Fatalf("configured origin %q accepted", allowed)
}
}
request.Header.Set("Origin", "https://user@example.test")
if SameOrigin(request, "https://example.test") {
t.Fatal("origin containing user information accepted")
}
}
func TestCSRFIsPurposeBound(t *testing.T) {
secret := []byte("0123456789abcdef0123456789abcdef")
token, err := CSRFToken(secret, "account:update")
if err != nil {
t.Fatal(err)
}
if !VerifyCSRF(secret, "account:update", token) {
t.Fatal("valid token rejected")
}
if VerifyCSRF(secret, "account:delete", token) {
t.Fatal("cross-purpose token accepted")
}
}
func TestSafeLocalRedirect(t *testing.T) {
for _, unsafe := range []string{"https://attacker.test/", "//attacker.test/", "/\\attacker", "javascript:alert(1)"} {
if got := SafeLocalRedirect(unsafe, "/home"); got != "/home" {
t.Fatalf("%q => %q", unsafe, got)
}
}
if got := SafeLocalRedirect("/items?page=2", "/"); got != "/items?page=2" {
t.Fatalf("got=%q", got)
}
}
func TestLimiterRefillsAndStaysBounded(t *testing.T) {
now := time.Unix(100, 0)
limiter, err := NewLimiter(LimitConfig{RatePerSecond: 1, Burst: 2, MaxEntries: 2, Now: func() time.Time { return now }})
if err != nil {
t.Fatal(err)
}
if !limiter.Allow("a") || !limiter.Allow("a") || limiter.Allow("a") {
t.Fatal("unexpected initial budget")
}
now = now.Add(time.Second)
if !limiter.Allow("a") {
t.Fatal("token did not refill")
}
_ = limiter.Allow("b")
_ = limiter.Allow("c")
if len(limiter.entries) != 2 {
t.Fatalf("entries=%d", len(limiter.entries))
}
}