upaas/internal/middleware/apicsrf_test.go
user efa8f51310
All checks were successful
Check / check (pull_request) Successful in 11m36s
Add API CSRF protection via X-Requested-With header (closes #112)
- Add APICSRFProtection middleware requiring X-Requested-With header on
  state-changing API requests (POST, PUT, DELETE, PATCH)
- Apply middleware to all /api/v1 routes
- Upgrade session cookie SameSite from Lax to Strict (defense-in-depth)
- Add X-Requested-With to CORS allowed headers
- Add tests for the new middleware

Browsers cannot send custom headers cross-origin without CORS preflight,
which effectively blocks CSRF attacks via cookie-based session auth.
2026-02-20 05:33:33 -08:00

80 lines
2.2 KiB
Go

package middleware //nolint:testpackage // tests internal middleware behavior
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func newAPICSRFTestMiddleware() *Middleware {
return newCORSTestMiddleware("")
}
func TestAPICSRFProtection_SafeMethods_Allowed(t *testing.T) {
t.Parallel()
m := newAPICSRFTestMiddleware()
handler := m.APICSRFProtection()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
for _, method := range []string{http.MethodGet, http.MethodHead, http.MethodOptions} {
req := httptest.NewRequest(method, "/api/v1/apps", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code,
"expected %s to be allowed without X-Requested-With", method)
}
}
func TestAPICSRFProtection_POST_WithoutHeader_Blocked(t *testing.T) {
t.Parallel()
m := newAPICSRFTestMiddleware()
handler := m.APICSRFProtection()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodPost, "/api/v1/apps", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusForbidden, rec.Code)
assert.Contains(t, rec.Body.String(), "X-Requested-With")
}
func TestAPICSRFProtection_POST_WithHeader_Allowed(t *testing.T) {
t.Parallel()
m := newAPICSRFTestMiddleware()
handler := m.APICSRFProtection()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodPost, "/api/v1/apps", nil)
req.Header.Set("X-Requested-With", "XMLHttpRequest")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
}
func TestAPICSRFProtection_DELETE_WithoutHeader_Blocked(t *testing.T) {
t.Parallel()
m := newAPICSRFTestMiddleware()
handler := m.APICSRFProtection()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodDelete, "/api/v1/apps/1", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusForbidden, rec.Code)
}