diff --git a/Makefile b/Makefile index 6e73cf66f2..f9c44bb761 100644 --- a/Makefile +++ b/Makefile @@ -108,7 +108,7 @@ dev-extension: build-server build-client ## Run the extension in development mod ##@ Docs .PHONY: docs-build docs-validate docs-clean docs-validate-clean docs-build: init-dist ## Build docs - go mod download -x + go mod download cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 -p pascalcase --markdownFiles ./ docs-validate: docs-build ## Validate docs diff --git a/api/http/csrf/csrf.go b/api/http/csrf/csrf.go index 0616eb69c1..d71093bfc8 100644 --- a/api/http/csrf/csrf.go +++ b/api/http/csrf/csrf.go @@ -1,30 +1,13 @@ package csrf import ( - "crypto/rand" - "errors" "fmt" "net/http" - "net/url" "os" - "github.com/portainer/portainer/api/http/security" - "github.com/portainer/portainer/pkg/featureflags" - httperror "github.com/portainer/portainer/pkg/libhttp/error" - - gcsrf "github.com/gorilla/csrf" "github.com/rs/zerolog/log" - "github.com/urfave/negroni" ) -const csrfSkipHeader = "X-CSRF-Token-Skip" - -// SkipCSRFToken signals that the X-CSRF-Token header should not be sent in the response. -// Deprecated: only meaningful when the "legacy-csrf" feature flag is enabled. -func SkipCSRFToken(w http.ResponseWriter) { - w.Header().Set(csrfSkipHeader, "1") -} - func WithProtect(handler http.Handler, trustedOrigins []string) (http.Handler, error) { // DOCKER_EXTENSION=1 is set in build/docker-extension/docker-compose.yml isDockerDesktopExtension := false @@ -32,10 +15,6 @@ func WithProtect(handler http.Handler, trustedOrigins []string) (http.Handler, e isDockerDesktopExtension = true } - if featureflags.IsEnabled("legacy-csrf") { - return withLegacyProtect(handler, trustedOrigins, isDockerDesktopExtension) - } - cop := http.NewCrossOriginProtection() for _, origin := range trustedOrigins { if err := cop.AddTrustedOrigin(origin); err != nil { @@ -58,14 +37,7 @@ func WithProtect(handler http.Handler, trustedOrigins []string) (http.Handler, e protected := cop.Handler(handler) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - skip, err := security.ShouldSkipCSRFCheck(r, isDockerDesktopExtension) - if err != nil { - httperror.WriteError(w, http.StatusForbidden, err.Error(), err) - - return - } - - if skip { + if isDockerDesktopExtension { handler.ServeHTTP(w, r) return @@ -74,99 +46,3 @@ func WithProtect(handler http.Handler, trustedOrigins []string) (http.Handler, e protected.ServeHTTP(w, r) }), nil } - -// Deprecated: use WithProtect without the "legacy-csrf" feature flag instead. -func withLegacyProtect(handler http.Handler, trustedOrigins []string, isDockerDesktopExtension bool) (http.Handler, error) { - handler = withLegacySendCSRFToken(handler) - - token := make([]byte, 32) - if _, err := rand.Read(token); err != nil { - return nil, fmt.Errorf("failed to generate CSRF token: %w", err) - } - - // gorilla/csrf compares referer.Host against trusted origin entries, so it - // needs bare host[:port] values rather than full scheme://host[:port] origins. - legacyOrigins := make([]string, len(trustedOrigins)) - for i, origin := range trustedOrigins { - parsed, err := url.Parse(origin) - if err != nil { - return nil, fmt.Errorf("failed to parse trusted origin %q: %w", origin, err) - } - - legacyOrigins[i] = parsed.Host - } - - handler = gcsrf.Protect( - token, - gcsrf.Path("/"), - gcsrf.Secure(false), - gcsrf.TrustedOrigins(legacyOrigins), - gcsrf.ErrorHandler(withLegacyErrorHandler(trustedOrigins)), - )(handler) - - return withLegacySkipCSRF(handler, isDockerDesktopExtension), nil -} - -// Deprecated: use WithProtect without the "legacy-csrf" feature flag instead. -func withLegacySendCSRFToken(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - sw := negroni.NewResponseWriter(w) - - sw.Before(func(sw negroni.ResponseWriter) { - if len(sw.Header().Get(csrfSkipHeader)) > 0 { - sw.Header().Del(csrfSkipHeader) - - return - } - - if statusCode := sw.Status(); statusCode >= 200 && statusCode < 300 { - sw.Header().Set("X-CSRF-Token", gcsrf.Token(r)) - } - }) - - handler.ServeHTTP(sw, r) - }) -} - -// Deprecated: use WithProtect without the "legacy-csrf" feature flag instead. -func withLegacySkipCSRF(handler http.Handler, isDockerDesktopExtension bool) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - skip, err := security.ShouldSkipCSRFCheck(r, isDockerDesktopExtension) - if err != nil { - httperror.WriteError(w, http.StatusForbidden, err.Error(), err) - - return - } - - if skip { - r = gcsrf.UnsafeSkipCheck(r) - } - - handler.ServeHTTP(w, r) - }) -} - -// Deprecated: use WithProtect without the "legacy-csrf" feature flag instead. -func withLegacyErrorHandler(trustedOrigins []string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := gcsrf.FailureReason(r) - - if errors.Is(err, gcsrf.ErrBadOrigin) || errors.Is(err, gcsrf.ErrBadReferer) || errors.Is(err, gcsrf.ErrNoReferer) { - log.Error().Err(err). - Str("request_url", r.URL.String()). - Str("host", r.Host). - Str("x_forwarded_proto", r.Header.Get("X-Forwarded-Proto")). - Str("forwarded", r.Header.Get("Forwarded")). - Str("origin", r.Header.Get("Origin")). - Str("referer", r.Header.Get("Referer")). - Strs("trusted_origins", trustedOrigins). - Msg("Failed to validate Origin or Referer") - } - - http.Error( - w, - http.StatusText(http.StatusForbidden)+" - "+err.Error(), - http.StatusForbidden, - ) - }) -} diff --git a/api/http/csrf/csrf_test.go b/api/http/csrf/csrf_test.go index 034a882830..34d1d9cf24 100644 --- a/api/http/csrf/csrf_test.go +++ b/api/http/csrf/csrf_test.go @@ -154,51 +154,6 @@ func TestWithProtect_allowsPostFromTrustedOrigin(t *testing.T) { require.Equal(t, http.StatusOK, rr.Code) } -func TestWithProtect_skipsCsrfForApiKey(t *testing.T) { - t.Parallel() - - handler, err := WithProtect(okHandler, nil) - require.NoError(t, err) - - req := httptest.NewRequest(http.MethodPost, "/", nil) - req.Header.Set("Sec-Fetch-Site", "cross-site") - req.Header.Set("X-API-KEY", "my-api-key") - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) -} - -func TestWithProtect_skipsCsrfForBearerToken(t *testing.T) { - t.Parallel() - - handler, err := WithProtect(okHandler, nil) - require.NoError(t, err) - - req := httptest.NewRequest(http.MethodPost, "/", nil) - req.Header.Set("Sec-Fetch-Site", "cross-site") - req.Header.Set("Authorization", "Bearer some-token") - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) -} - -func TestWithProtect_forbidsBothApiKeyAndBearerToken(t *testing.T) { - t.Parallel() - - handler, err := WithProtect(okHandler, nil) - require.NoError(t, err) - - req := httptest.NewRequest(http.MethodPost, "/", nil) - req.Header.Set("X-API-KEY", "my-api-key") - req.Header.Set("Authorization", "Bearer some-token") - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - require.Equal(t, http.StatusForbidden, rr.Code) -} - func TestWithProtect_enforcesCsrfForCookieAuth(t *testing.T) { t.Parallel() @@ -213,88 +168,3 @@ func TestWithProtect_enforcesCsrfForCookieAuth(t *testing.T) { handler.ServeHTTP(rr, req) require.Equal(t, http.StatusForbidden, rr.Code) } - -func TestWithLegacyProtect_noError_noOrigins(t *testing.T) { - t.Parallel() - - _, err := withLegacyProtect(okHandler, nil, false) - require.NoError(t, err) -} - -func TestWithLegacyProtect_noError_schemeHostOrigin(t *testing.T) { - t.Parallel() - - _, err := withLegacyProtect(okHandler, []string{"https://example.com"}, false) - require.NoError(t, err) -} - -func TestWithLegacyProtect_noError_schemeHostPortOrigin(t *testing.T) { - t.Parallel() - - _, err := withLegacyProtect(okHandler, []string{"https://example.com:3000"}, false) - require.NoError(t, err) -} - -func TestWithLegacyProtect_noError_multipleOrigins(t *testing.T) { - t.Parallel() - - _, err := withLegacyProtect(okHandler, []string{"https://example.com", "http://internal.example.com:8080"}, false) - require.NoError(t, err) -} - -func TestWithLegacyProtect_safeMethodsAlwaysAllowed(t *testing.T) { - t.Parallel() - - handler, err := withLegacyProtect(okHandler, nil, false) - require.NoError(t, err) - - for _, method := range []string{http.MethodGet, http.MethodHead, http.MethodOptions} { - req := httptest.NewRequest(method, "/", nil) - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code, "method %s should be allowed", method) - } -} - -func TestWithLegacyProtect_blocksPostWithoutToken(t *testing.T) { - t.Parallel() - - handler, err := withLegacyProtect(okHandler, nil, false) - require.NoError(t, err) - - req := httptest.NewRequest(http.MethodPost, "/", nil) - req.AddCookie(&http.Cookie{Name: portainer.AuthCookieKey, Value: "some-token"}) - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - require.Equal(t, http.StatusForbidden, rr.Code) -} - -func TestWithLegacyProtect_skipsCsrfForApiKey(t *testing.T) { - t.Parallel() - - handler, err := withLegacyProtect(okHandler, nil, false) - require.NoError(t, err) - - req := httptest.NewRequest(http.MethodPost, "/", nil) - req.Header.Set("X-API-KEY", "my-api-key") - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) -} - -func TestWithLegacyProtect_skipsCsrfForBearerToken(t *testing.T) { - t.Parallel() - - handler, err := withLegacyProtect(okHandler, nil, false) - require.NoError(t, err) - - req := httptest.NewRequest(http.MethodPost, "/", nil) - req.Header.Set("Authorization", "Bearer some-token") - - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) -} diff --git a/api/http/middlewares/plaintext_http_request.go b/api/http/middlewares/plaintext_http_request.go deleted file mode 100644 index 64b1833cb1..0000000000 --- a/api/http/middlewares/plaintext_http_request.go +++ /dev/null @@ -1,75 +0,0 @@ -package middlewares - -import ( - "net/http" - "slices" - "strings" - - "github.com/gorilla/csrf" -) - -var ( - // Idempotent (safe) methods as defined by RFC7231 section 4.2.2. - safeMethods = []string{"GET", "HEAD", "OPTIONS", "TRACE"} -) - -type plainTextHTTPRequestHandler struct { - next http.Handler -} - -// parseForwardedHeaderProto parses the Forwarded header and extracts the protocol. -// The Forwarded header format supports: -// - Single proxy: Forwarded: by=;for=;host=;proto= -// - Multiple proxies: Forwarded: for=192.0.2.43, for=198.51.100.17 -// We take the first (leftmost) entry as it represents the original client -func parseForwardedHeaderProto(forwarded string) string { - if forwarded == "" { - return "" - } - - // Parse the first part (leftmost proxy, closest to original client) - firstPart, _, _ := strings.Cut(forwarded, ",") - firstPart = strings.TrimSpace(firstPart) - - // Split by semicolon to get key-value pairs within this proxy entry - // Format: key=value;key=value;key=value - for pair := range strings.SplitSeq(firstPart, ";") { - // Split by equals sign to separate key and value - key, value, found := strings.Cut(pair, "=") - if !found { - continue - } - - if strings.EqualFold(strings.TrimSpace(key), "proto") { - return strings.Trim(strings.TrimSpace(value), `"'`) - } - } - - return "" -} - -// isHTTPSRequest checks if the original request was made over HTTPS -// by examining both X-Forwarded-Proto and Forwarded headers -func isHTTPSRequest(r *http.Request) bool { - return strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") || - strings.EqualFold(parseForwardedHeaderProto(r.Header.Get("Forwarded")), "https") -} - -func (h *plainTextHTTPRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if slices.Contains(safeMethods, r.Method) { - h.next.ServeHTTP(w, r) - return - } - - req := r - // If original request was HTTPS (via proxy), keep CSRF checks. - if !isHTTPSRequest(r) { - req = csrf.PlaintextHTTPRequest(r) - } - - h.next.ServeHTTP(w, req) -} - -func PlaintextHTTPRequest(next http.Handler) http.Handler { - return &plainTextHTTPRequestHandler{next: next} -} diff --git a/api/http/middlewares/plaintext_http_request_test.go b/api/http/middlewares/plaintext_http_request_test.go deleted file mode 100644 index 58650ffad0..0000000000 --- a/api/http/middlewares/plaintext_http_request_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package middlewares - -import ( - "testing" -) - -var tests = []struct { - name string - forwarded string - expected string -}{ - { - name: "empty header", - forwarded: "", - expected: "", - }, - { - name: "single proxy with proto=https", - forwarded: "proto=https", - expected: "https", - }, - { - name: "single proxy with proto=http", - forwarded: "proto=http", - expected: "http", - }, - { - name: "single proxy with multiple directives", - forwarded: "for=192.0.2.60;proto=https;by=203.0.113.43", - expected: "https", - }, - { - name: "single proxy with proto in middle", - forwarded: "for=192.0.2.60;proto=https;host=example.com", - expected: "https", - }, - { - name: "single proxy with proto at end", - forwarded: "for=192.0.2.60;host=example.com;proto=https", - expected: "https", - }, - { - name: "multiple proxies - takes first", - forwarded: "proto=https, proto=http", - expected: "https", - }, - { - name: "multiple proxies with complex format", - forwarded: "for=192.0.2.43;proto=https, for=198.51.100.17;proto=http", - expected: "https", - }, - { - name: "multiple proxies with for directive only", - forwarded: "for=192.0.2.43, for=198.51.100.17", - expected: "", - }, - { - name: "multiple proxies with proto only in second", - forwarded: "for=192.0.2.43, proto=https", - expected: "", - }, - { - name: "multiple proxies with proto only in first", - forwarded: "proto=https, for=198.51.100.17", - expected: "https", - }, - { - name: "quoted protocol value", - forwarded: "proto=\"https\"", - expected: "https", - }, - { - name: "single quoted protocol value", - forwarded: "proto='https'", - expected: "https", - }, - { - name: "mixed case protocol", - forwarded: "proto=HTTPS", - expected: "HTTPS", - }, - { - name: "no proto directive", - forwarded: "for=192.0.2.60;by=203.0.113.43", - expected: "", - }, - { - name: "empty proto value", - forwarded: "proto=", - expected: "", - }, - { - name: "whitespace around values", - forwarded: " proto = https ", - expected: "https", - }, - { - name: "whitespace around semicolons", - forwarded: "for=192.0.2.60 ; proto=https ; by=203.0.113.43", - expected: "https", - }, - { - name: "whitespace around commas", - forwarded: "proto=https , proto=http", - expected: "https", - }, - { - name: "IPv6 address in for directive", - forwarded: "for=\"[2001:db8:cafe::17]:4711\";proto=https", - expected: "https", - }, - { - name: "complex multiple proxies with IPv6", - forwarded: "for=192.0.2.43;proto=https, for=\"[2001:db8:cafe::17]\";proto=http", - expected: "https", - }, - { - name: "obfuscated identifiers", - forwarded: "for=_mdn;proto=https", - expected: "https", - }, - { - name: "unknown identifier", - forwarded: "for=unknown;proto=https", - expected: "https", - }, - { - name: "malformed key-value pair", - forwarded: "proto", - expected: "", - }, - { - name: "malformed key-value pair with equals", - forwarded: "proto=", - expected: "", - }, - { - name: "multiple equals signs", - forwarded: "proto=https=extra", - expected: "https=extra", - }, - { - name: "mixed case directive name", - forwarded: "PROTO=https", - expected: "https", - }, - { - name: "mixed case directive name with spaces", - forwarded: " Proto = https ", - expected: "https", - }, -} - -func TestParseForwardedHeaderProto(t *testing.T) { - t.Parallel() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := parseForwardedHeaderProto(tt.forwarded) - if result != tt.expected { - t.Errorf("parseForwardedHeader(%q) = %q, want %q", tt.forwarded, result, tt.expected) - } - }) - } -} - -func FuzzParseForwardedHeaderProto(f *testing.F) { - for _, t := range tests { - f.Add(t.forwarded) - } - - f.Fuzz(func(t *testing.T, forwarded string) { - parseForwardedHeaderProto(forwarded) - }) -} diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index 1a60d49123..cc831a279d 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -307,6 +307,14 @@ func (bouncer *RequestBouncer) mwIsTeamLeader(next http.Handler) http.Handler { // A result of a first succeeded token lookup would be used for the authentication. func (bouncer *RequestBouncer) mwAuthenticateFirst(tokenLookups []tokenLookup, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, hasAPIKey := extractAPIKey(r) + _, hasBearerToken := extractBearerToken(r) + if hasAPIKey && hasBearerToken { + httperror.WriteError(w, http.StatusUnauthorized, "API key and auth header are not allowed at the same time", httperrors.ErrUnauthorized) + + return + } + var token *portainer.TokenData for _, lookup := range tokenLookups { @@ -570,40 +578,3 @@ func (bouncer *RequestBouncer) EdgeComputeOperation(next http.Handler) http.Hand next.ServeHTTP(w, r) }) } - -// ShouldSkipCSRFCheck checks if the CSRF check should be skipped -// -// It returns true if the request has no cookie token and has either (but not both): -// - an api key header -// - an auth header -// if it has both headers, an error is returned -// -// we allow CSRF check to be skipped for the following reasons: -// - public routes -// - kubectl - a bearer token is needed, and no csrf token can be sent -// - api token -// - docker desktop extension -func ShouldSkipCSRFCheck(r *http.Request, isDockerDesktopExtension bool) (bool, error) { - if isDockerDesktopExtension { - return true, nil - } - - cookie, _ := r.Cookie(portainer.AuthCookieKey) - hasCookie := cookie != nil && cookie.Value != "" - - if hasCookie { - return false, nil - } - - apiKey := r.Header.Get(apiKeyHeader) - hasApiKey := apiKey != "" - - authHeader := r.Header.Get(jwtTokenHeader) - hasAuthHeader := authHeader != "" - - if hasApiKey && hasAuthHeader { - return false, errors.New("api key and auth header are not allowed at the same time") - } - - return true, nil -} diff --git a/api/http/security/bouncer_test.go b/api/http/security/bouncer_test.go index ab3e1ba33c..cd25b8b190 100644 --- a/api/http/security/bouncer_test.go +++ b/api/http/security/bouncer_test.go @@ -399,81 +399,25 @@ func Test_apiKeyLookup(t *testing.T) { }) } -func Test_ShouldSkipCSRFCheck(t *testing.T) { +func Test_mwAuthenticateFirst_rejectsBothAPIKeyAndBearerToken(t *testing.T) { t.Parallel() - tt := []struct { - name string - cookieValue string - apiKey string - authHeader string - isDockerDesktopExtension bool - expectedResult bool - expectedError bool - }{ - { - name: "Should return false (not skip) when cookie is present", - cookieValue: "test-cookie", - isDockerDesktopExtension: false, - }, - { - name: "Should return true (skip) when cookie is present and docker desktop extension is true", - cookieValue: "test-cookie", - isDockerDesktopExtension: true, - expectedResult: true, - }, - { - name: "Should return true (skip) when cookie is not present", - cookieValue: "", - isDockerDesktopExtension: false, - expectedResult: true, - }, - { - name: "Should return true (skip) when api key is present", - cookieValue: "", - apiKey: "test-api-key", - isDockerDesktopExtension: false, - expectedResult: true, - }, - { - name: "Should return true (skip) when auth header is present", - cookieValue: "", - authHeader: "test-auth-header", - isDockerDesktopExtension: false, - expectedResult: true, - }, - { - name: "Should return false (not skip) and error when both api key and auth header are present", - cookieValue: "", - apiKey: "test-api-key", - authHeader: "test-auth-header", - isDockerDesktopExtension: false, - expectedError: true, - }, - } + _, store := datastore.MustNewTestStore(t, true, true) - for _, test := range tt { - t.Run(test.name, func(t *testing.T) { - is := assert.New(t) - req := httptest.NewRequest(http.MethodGet, "/", nil) - if test.cookieValue != "" { - req.AddCookie(&http.Cookie{Name: portainer.AuthCookieKey, Value: test.cookieValue}) - } - if test.apiKey != "" { - req.Header.Set(apiKeyHeader, test.apiKey) - } - if test.authHeader != "" { - req.Header.Set(jwtTokenHeader, test.authHeader) - } + jwtService, err := jwt.NewService("1h", store) + require.NoError(t, err) - result, err := ShouldSkipCSRFCheck(req, test.isDockerDesktopExtension) - is.Equal(test.expectedResult, result) - if test.expectedError { - require.Error(t, err) - } else { - require.NoError(t, err) - } - }) - } + apiKeyService := apikey.NewAPIKeyService(nil, nil) + bouncer := NewRequestBouncer(t.Context(), store, jwtService, apiKeyService) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set(apiKeyHeader, "test-api-key") + req.Header.Set(jwtTokenHeader, "Bearer test-token") + + rr := httptest.NewRecorder() + h := bouncer.mwAuthenticateFirst(nil, testHandler200) + h.ServeHTTP(rr, req) + + require.Equal(t, http.StatusUnauthorized, rr.Code) } func TestJWTRevocation(t *testing.T) { diff --git a/api/http/server.go b/api/http/server.go index 72354b3d23..4f960a7e81 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -346,7 +346,7 @@ func (server *Server) Start(ctx context.Context) error { log.Info().Str("bind_address", server.BindAddress).Msg("starting HTTP server") httpServer := &http.Server{ Addr: server.BindAddress, - Handler: middlewares.PlaintextHTTPRequest(handler), + Handler: handler, ErrorLog: errorLogger, } diff --git a/api/portainer.go b/api/portainer.go index 79a691732d..0c45a0c97c 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1977,7 +1977,7 @@ const ( ) // List of supported features -var SupportedFeatureFlags = []featureflags.Feature{"hsts", "csp", "legacy-csrf"} +var SupportedFeatureFlags = []featureflags.Feature{"hsts", "csp"} const ( _ AuthenticationMethod = iota diff --git a/app/app.js b/app/app.js index 1bad4eeaec..f5286f5884 100644 --- a/app/app.js +++ b/app/app.js @@ -33,7 +33,5 @@ export function onStartupAngular($rootScope, $state, cfpLoadingBar, $transitions if (type && hasNoContentType) { jqXhr.setRequestHeader('Content-Type', 'application/json'); } - const csrfCookie = window.cookieStore.get('_gorilla_csrf'); - jqXhr.setRequestHeader('X-CSRF-Token', csrfCookie); }); } diff --git a/app/config.js b/app/config.js index 108ffb9a0b..fbad8d4074 100644 --- a/app/config.js +++ b/app/config.js @@ -1,5 +1,4 @@ import { agentInterceptor } from '@/react/portainer/services/axios/axios'; -import { csrfInterceptor, csrfTokenReaderInterceptorAngular } from './portainer/services/csrf'; import { dispatchCacheRefreshEventIfNeeded } from './portainer/services/http-request.helper'; /* @ngInject */ @@ -26,11 +25,6 @@ export function configApp($urlRouterProvider, $httpProvider, localStorageService request: agentInterceptor, })); - $httpProvider.interceptors.push(() => ({ - response: csrfTokenReaderInterceptorAngular, - request: csrfInterceptor, - })); - $uibTooltipProvider.setTriggers({ mouseenter: 'mouseleave', click: 'click', diff --git a/app/portainer/services/csrf.ts b/app/portainer/services/csrf.ts deleted file mode 100644 index 07c8be7ad1..0000000000 --- a/app/portainer/services/csrf.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { AxiosResponse, InternalAxiosRequestConfig } from 'axios'; -import { CacheAxiosResponse } from 'axios-cache-interceptor'; -import { IHttpResponse } from 'angular'; - -import axios from './axios/axios'; - -axios.interceptors.response.use(csrfTokenReaderInterceptor); -axios.interceptors.request.use(csrfInterceptor); - -let csrfToken: string | null = null; - -export function csrfTokenReaderInterceptor( - config: CacheAxiosResponse | AxiosResponse -) { - const csrfTokenHeader = config.headers['x-csrf-token']; - if (csrfTokenHeader) { - csrfToken = csrfTokenHeader; - } - return config; -} - -export function csrfTokenReaderInterceptorAngular( - config: IHttpResponse -) { - const csrfTokenHeader = config.headers('x-csrf-token'); - if (csrfTokenHeader) { - csrfToken = csrfTokenHeader; - } - return config; -} - -export function csrfInterceptor(config: InternalAxiosRequestConfig) { - if (!csrfToken) { - return config; - } - - const newConfig = { ...config }; - newConfig.headers['X-CSRF-Token'] = csrfToken; - return newConfig; -} diff --git a/go.mod b/go.mod index 0067b17179..7d72ecced3 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,6 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 - github.com/gorilla/csrf v1.7.3 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 github.com/hashicorp/golang-lru v0.6.0 @@ -55,7 +54,6 @@ require ( github.com/segmentio/encoding v0.5.3 github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 - github.com/urfave/negroni v1.0.0 github.com/viney-shih/go-lock v1.1.1 go.etcd.io/bbolt v1.4.3 go.podman.io/image/v5 v5.37.0 @@ -75,8 +73,6 @@ require ( oras.land/oras-go/v2 v2.6.0 ) -require github.com/gorilla/securecookie v1.1.2 // indirect - require ( cloud.google.com/go/auth v0.18.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect diff --git a/go.sum b/go.sum index bfea2546d5..1943ac0009 100644 --- a/go.sum +++ b/go.sum @@ -462,8 +462,6 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef h1:xpF9fUHpoIrrjX24DURVKiwHcFpw19ndIs+FwTSMbno= github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -478,15 +476,11 @@ github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5 github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= github.com/gophercloud/gophercloud/v2 v2.10.0 h1:NRadC0aHNvy4iMoFXj5AFiPmut/Sj3hAPAo9B59VMGc= github.com/gophercloud/gophercloud/v2 v2.10.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= -github.com/gorilla/csrf v1.7.3 h1:BHWt6FTLZAb2HtWT5KDBf6qgpZzvtbp9QWDRKZMXJC0= -github.com/gorilla/csrf v1.7.3/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= -github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= @@ -953,8 +947,6 @@ github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Q github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= -github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/viney-shih/go-lock v1.1.1 h1:SwzDPPAiHpcwGCr5k8xD15d2gQSo8d4roRYd7TDV2eI=