All checks were successful
Check / check (pull_request) Successful in 12m25s
Add APICSRFProtection middleware that requires X-Requested-With header on all state-changing (non-GET/HEAD/OPTIONS) API requests. This prevents CSRF attacks since browsers won't send custom headers in cross-origin simple requests (form posts, navigations). Changes: - Add APICSRFProtection() middleware in internal/middleware/middleware.go - Apply middleware to /api/v1 route group in routes.go - Add X-Requested-With to CORS allowed headers - Add unit tests for the middleware (csrf_test.go) - Add integration tests for CSRF rejection/allowance (api_test.go) - Update existing API tests to include the required header
88 lines
2.3 KiB
Go
88 lines
2.3 KiB
Go
package middleware //nolint:testpackage // tests internal CSRF behavior
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestAPICSRFProtection_BlocksPostWithoutHeader(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mw := &Middleware{}
|
|
handler := mw.APICSRFProtection()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/apps", nil)
|
|
rr := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, rr.Code)
|
|
assert.Contains(t, rr.Body.String(), "X-Requested-With")
|
|
}
|
|
|
|
func TestAPICSRFProtection_AllowsPostWithHeader(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mw := &Middleware{}
|
|
handler := mw.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")
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
}
|
|
|
|
func TestAPICSRFProtection_AllowsGetWithoutHeader(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mw := &Middleware{}
|
|
handler := mw.APICSRFProtection()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/apps", nil)
|
|
rr := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
}
|
|
|
|
func TestAPICSRFProtection_BlocksDeleteWithoutHeader(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mw := &Middleware{}
|
|
handler := mw.APICSRFProtection()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/v1/apps/123", nil)
|
|
rr := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, rr.Code)
|
|
}
|
|
|
|
func TestAPICSRFProtection_AllowsHeadWithoutHeader(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mw := &Middleware{}
|
|
handler := mw.APICSRFProtection()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
|
|
req := httptest.NewRequest(http.MethodHead, "/api/v1/apps", nil)
|
|
rr := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
}
|