From 56fec3fcb5703f70c0f2e67eee2ae6e73e401cd0 Mon Sep 17 00:00:00 2001 From: Jian Qiu Date: Fri, 4 Sep 2026 20:48:18 +0800 Subject: [PATCH] fix(apigateway): authenticate csrf resource requests with the caller session (#25566) Align the csrf resource handler with other API resource handlers by requiring a token and forwarding with the caller's own session. Co-authored-by: Qiu Jian Co-authored-by: Cursor --- pkg/apigateway/handler/csrfexempt.go | 101 ++++------------------ pkg/apigateway/handler/csrfexempt_test.go | 74 ++++++++++++++++ 2 files changed, 92 insertions(+), 83 deletions(-) create mode 100644 pkg/apigateway/handler/csrfexempt_test.go diff --git a/pkg/apigateway/handler/csrfexempt.go b/pkg/apigateway/handler/csrfexempt.go index 2d0ad5ab51..43cb6556dd 100644 --- a/pkg/apigateway/handler/csrfexempt.go +++ b/pkg/apigateway/handler/csrfexempt.go @@ -21,11 +21,9 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/log" "yunion.io/x/pkg/appctx" - "yunion.io/x/pkg/util/sets" "yunion.io/x/onecloud/pkg/appsrv" "yunion.io/x/onecloud/pkg/httperrors" - "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/mcclient/auth" "yunion.io/x/onecloud/pkg/mcclient/modulebase" ) @@ -39,103 +37,40 @@ func NewCSRFResourceHandler(prefix string) *CSRFResourceHandler { } func (h *CSRFResourceHandler) Bind(app *appsrv.Application) { - h.AddByMethod(GET, nil, NewHP(getHandlerCsrf, APIVer, "csrf", ResName, ResID)) + h.AddByMethod(GET, FetchAuthToken, NewHP(getHandlerCsrf, APIVer, "csrf", ResName, ResID)) h.SHandlers.Bind(app) } -func getAdminSession(ctx context.Context, region string, w http.ResponseWriter) *mcclient.ClientSession { - adminToken := auth.AdminCredential() - if adminToken == nil { - httperrors.NotFoundError(ctx, w, "get admin credential is nil") - return nil +func getHandlerCsrf(ctx context.Context, w http.ResponseWriter, r *http.Request) { + token := AppContextToken(ctx) + if token == nil { + httperrors.UnauthorizedError(ctx, w, "No valid auth token found") + return } - regions := adminToken.GetRegions() - log.Infof("CSRF regions: %v", regions) - if len(regions) == 0 { - httperrors.NotFoundError(ctx, w, "no usable regions, please contact admin") - return nil + params := appctx.AppContextParams(ctx) + region := r.URL.Query().Get("region") + if len(region) < 1 { + httperrors.NotFoundError(ctx, w, "region %s is empty", region) + return } - ret, _ := sets.InArray(region, regions) - if !ret { - httperrors.NotFoundError(ctx, w, "illegal region %s, please contact admin", region) + session := auth.GetSession(ctx, token, region) + if session == nil { + httperrors.GeneralServerError(ctx, w, httperrors.ErrInvalidCredential) + return } - s := auth.GetAdminSession(ctx, region) - return s -} - -func fetchEnv3Csrf(ctx context.Context, w http.ResponseWriter, r *http.Request) (modulebase.Manager, modulebase.Manager, modulebase.Manager, *mcclient.ClientSession, map[string]string, jsonutils.JSONObject, jsonutils.JSONObject) { - module, module2, session, params, query, body := fetchEnv2Csrf(ctx, w, r) - if module == nil || module2 == nil { - return nil, nil, nil, nil, nil, nil, nil - } - module3, e := modulebase.GetModule(session, params[ResName3]) - if e != nil || module == nil { - httperrors.NotFoundError(ctx, w, "resource %s not found", params[ResName3]) - return nil, nil, nil, nil, nil, nil, nil - } - return module, module2, module3, session, params, query, body -} - -func fetchEnv2Csrf(ctx context.Context, w http.ResponseWriter, r *http.Request) (modulebase.Manager, modulebase.Manager, *mcclient.ClientSession, map[string]string, jsonutils.JSONObject, jsonutils.JSONObject) { - module, session, params, query, body := fetchEnvCsrf(ctx, w, r) - if module == nil { - return nil, nil, nil, nil, nil, nil - } - module2, e := modulebase.GetModule(session, params[ResName2]) - if e != nil || module == nil { - httperrors.NotFoundError(ctx, w, "resource %s not found", params[ResName2]) - return nil, nil, nil, nil, nil, nil - } - return module, module2, session, params, query, body -} - -func fetchEnvCsrf(ctx context.Context, w http.ResponseWriter, r *http.Request) (modulebase.Manager, *mcclient.ClientSession, map[string]string, jsonutils.JSONObject, jsonutils.JSONObject) { - session, params, query, body := fetchEnvCsrf0(ctx, w, r) module, e := modulebase.GetModule(session, params[ResName]) if e != nil || module == nil { httperrors.NotFoundError(ctx, w, "resource %s not found", params[ResName]) - return nil, nil, nil, nil, nil - } - return module, session, params, query, body -} - -func fetchEnvCsrf0(ctx context.Context, w http.ResponseWriter, r *http.Request) (*mcclient.ClientSession, map[string]string, jsonutils.JSONObject, jsonutils.JSONObject) { - params := appctx.AppContextParams(ctx) - region := r.URL.Query().Get("region") - log.Println("csrf region from url:", region) - if len(region) < 1 { - httperrors.NotFoundError(ctx, w, "region %s is empty", region) - return nil, nil, nil, nil - } - log.Infof("csrf region from url: %s", region) - session := getAdminSession(ctx, region, w) - log.Infof("csrf got session: %s", region) - if session == nil { - return nil, nil, nil, nil + return } query, e := jsonutils.ParseQueryString(r.URL.RawQuery) if e != nil { log.Errorf("Parse query string %s: %v", r.URL.RawQuery, e) } - var body jsonutils.JSONObject = nil - if r.Method == PUT || r.Method == POST || r.Method == DELETE || r.Method == PATCH { - body, e = appsrv.FetchJSON(r) - if e != nil { - log.Errorf("Fail to decode JSON request body: %v", e) - } - } - return session, params, query, body -} - -func getHandlerCsrf(ctx context.Context, w http.ResponseWriter, r *http.Request) { - module, session, params, query, _ := fetchEnvCsrf(ctx, w, r) - if module == nil { - return - } obj, e := module.Get(session, params[ResID], query) if e != nil { httperrors.GeneralServerError(ctx, w, e) - } else { - appsrv.SendJSON(w, obj) + return } + appsrv.SendJSON(w, obj) } diff --git a/pkg/apigateway/handler/csrfexempt_test.go b/pkg/apigateway/handler/csrfexempt_test.go new file mode 100644 index 0000000000..049db197d3 --- /dev/null +++ b/pkg/apigateway/handler/csrfexempt_test.go @@ -0,0 +1,74 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "yunion.io/x/pkg/appctx" + + "yunion.io/x/onecloud/pkg/appsrv" + "yunion.io/x/onecloud/pkg/mcclient" +) + +func TestCsrfEndpointRequiresAuth(t *testing.T) { + app := appsrv.NewApplication("test-csrf", 1, 1, false) + NewCSRFResourceHandler("/api").Bind(app) + + for _, url := range []string{ + "/api/v1/csrf/servers/some-server?region=region0", + "/api/v1/csrf/networks/some-network", + } { + req := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + if w.Code != http.StatusUnauthorized { + t.Fatalf("GET %s: status = %d, want %d", url, w.Code, http.StatusUnauthorized) + } + } +} + +func TestCsrfEndpointRejectsInvalidCookie(t *testing.T) { + app := appsrv.NewApplication("test-csrf", 1, 1, false) + NewCSRFResourceHandler("/api").Bind(app) + + req := httptest.NewRequest("GET", "/api/v1/csrf/servers/some-server?region=region0", nil) + req.AddCookie(&http.Cookie{Name: "yunionauth", Value: "invalid-session-value"}) + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + if w.Code != http.StatusUnauthorized { + t.Fatalf("invalid cookie GET: status = %d, want %d", w.Code, http.StatusUnauthorized) + } +} + +func TestGetHandlerCsrfGuardClauses(t *testing.T) { + req := httptest.NewRequest("GET", "/api/v1/csrf/servers/some-server?region=region0", nil) + w := httptest.NewRecorder() + getHandlerCsrf(context.Background(), w, req) + if w.Code != http.StatusUnauthorized { + t.Fatalf("no token: status = %d, want %d", w.Code, http.StatusUnauthorized) + } + + ctx := context.WithValue(context.Background(), appctx.APP_CONTEXT_KEY_AUTH_TOKEN, &mcclient.SSimpleToken{}) + req = httptest.NewRequest("GET", "/api/v1/csrf/servers/some-server", nil) + w = httptest.NewRecorder() + getHandlerCsrf(ctx, w, req) + if w.Code != http.StatusNotFound { + t.Fatalf("empty region: status = %d, want %d", w.Code, http.StatusNotFound) + } +}