All checks were successful
Check / check (pull_request) Successful in 11m36s
- 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.
80 lines
2.2 KiB
Go
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)
|
|
}
|