From fb87235cdc220340677737bb70e88692c5579c6b Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Sun, 5 Jul 2026 00:18:23 +0200 Subject: [PATCH 1/2] Restrict sandbox auth bypass and add sandbox route audit --- .../transport/http/middleware/auth.go | 3 +- .../internal/transport/router/router_test.go | 76 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 backend/internal/transport/router/router_test.go diff --git a/backend/internal/transport/http/middleware/auth.go b/backend/internal/transport/http/middleware/auth.go index b7b2786..34cc4c5 100644 --- a/backend/internal/transport/http/middleware/auth.go +++ b/backend/internal/transport/http/middleware/auth.go @@ -89,10 +89,11 @@ func getDefaultConfig() *AuthConfig { {Path: "/api/sico/project/asset", Method: http.MethodPost}, {Path: "/api/sico/project/sas_asset", Method: http.MethodGet}, {Path: "/api/sico/project/asset", Method: http.MethodDelete}, + {Path: "/api/sico/sandbox/apply", Method: http.MethodPost}, + {Path: "/api/sico/sandbox/release", Method: http.MethodPost}, }, ExcludedPrefixes: []string{ "/api/sico/docs/", - "/api/sico/sandbox", }, JWTAuth: jwtx.New(jwtx.NewStoreWithCache(newCacheFromEnv())), } diff --git a/backend/internal/transport/router/router_test.go b/backend/internal/transport/router/router_test.go new file mode 100644 index 0000000..bf0b43a --- /dev/null +++ b/backend/internal/transport/router/router_test.go @@ -0,0 +1,76 @@ +// Copyright (c) 2026 Sico Authors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package router + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + + "sico-backend/internal/transport/http/middleware" +) + +func TestSandboxRoutesAreNotPublicByDefault(t *testing.T) { + gin.SetMode(gin.TestMode) + + engine := gin.New() + RegisterAPIs(engine) + + publicSandboxRoutes := map[string]bool{ + http.MethodPost + " /api/sico/sandbox/apply": true, + http.MethodPost + " /api/sico/sandbox/release": true, + } + + for _, route := range engine.Routes() { + if !strings.HasPrefix(route.Path, "/api/sico/sandbox") { + continue + } + + path := concreteSandboxRoute(route.Path) + key := route.Method + " " + path + + w := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(w) + ctx.Request = httptest.NewRequest(route.Method, path, nil) + + isPublic := middleware.IsAuthExcluded(ctx) + if publicSandboxRoutes[key] { + assert.True(t, isPublic, "sandbox route %s should remain public", key) + continue + } + + assert.False(t, isPublic, "sandbox route %s should be auth-protected", key) + } +} + +func concreteSandboxRoute(path string) string { + parts := strings.Split(path, "/") + for i, part := range parts { + if strings.HasPrefix(part, ":") || strings.HasPrefix(part, "*") { + parts[i] = "sample" + } + } + return strings.Join(parts, "/") +} From 3bc5649b951df4dfb1a23bec068738247be1774d Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Sun, 5 Jul 2026 00:29:21 +0200 Subject: [PATCH 2/2] Strengthen sandbox auth-exposure regression tests --- .../http/middleware/auth_exclusions_test.go | 86 +++++++++++++++++++ .../internal/transport/router/router_test.go | 35 +++++--- 2 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 backend/internal/transport/http/middleware/auth_exclusions_test.go diff --git a/backend/internal/transport/http/middleware/auth_exclusions_test.go b/backend/internal/transport/http/middleware/auth_exclusions_test.go new file mode 100644 index 0000000..e56f86f --- /dev/null +++ b/backend/internal/transport/http/middleware/auth_exclusions_test.go @@ -0,0 +1,86 @@ +// Copyright (c) 2026 Sico Authors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func TestIsAuthExcluded_OnlyAllowsExpectedSandboxEndpoints(t *testing.T) { + t.Run("sandbox endpoints", func(t *testing.T) { + tests := []struct { + name string + method string + path string + want bool + }{ + { + name: "sandbox apply POST is explicitly public", + method: http.MethodPost, + path: "/api/sico/sandbox/apply", + want: true, + }, + { + name: "sandbox release POST is explicitly public", + method: http.MethodPost, + path: "/api/sico/sandbox/release", + want: true, + }, + { + name: "sandbox list GET is protected", + method: http.MethodGet, + path: "/api/sico/sandbox/list", + want: false, + }, + { + name: "sandbox reset POST is protected", + method: http.MethodPost, + path: "/api/sico/sandbox/reset", + want: false, + }, + { + name: "sandbox instance vnc GET is protected", + method: http.MethodGet, + path: "/api/sico/sandbox/instance/abc/vnc", + want: false, + }, + { + name: "sandbox emulator api ANY path is protected", + method: http.MethodGet, + path: "/api/sico/sandbox/resources/emulator/abc/api/status", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + ctx.Request = httptest.NewRequest(tt.method, tt.path, nil) + assert.Equal(t, tt.want, IsAuthExcluded(ctx)) + }) + } + }) +} diff --git a/backend/internal/transport/router/router_test.go b/backend/internal/transport/router/router_test.go index bf0b43a..25c076b 100644 --- a/backend/internal/transport/router/router_test.go +++ b/backend/internal/transport/router/router_test.go @@ -42,6 +42,7 @@ func TestSandboxRoutesAreNotPublicByDefault(t *testing.T) { http.MethodPost + " /api/sico/sandbox/apply": true, http.MethodPost + " /api/sico/sandbox/release": true, } + discoveredSandboxRoutes := map[string]struct{}{} for _, route := range engine.Routes() { if !strings.HasPrefix(route.Path, "/api/sico/sandbox") { @@ -50,18 +51,25 @@ func TestSandboxRoutesAreNotPublicByDefault(t *testing.T) { path := concreteSandboxRoute(route.Path) key := route.Method + " " + path + discoveredSandboxRoutes[key] = struct{}{} - w := httptest.NewRecorder() - ctx, _ := gin.CreateTestContext(w) - ctx.Request = httptest.NewRequest(route.Method, path, nil) - - isPublic := middleware.IsAuthExcluded(ctx) - if publicSandboxRoutes[key] { - assert.True(t, isPublic, "sandbox route %s should remain public", key) - continue - } + assert.Equalf( + t, + publicSandboxRoutes[key], + isSandboxRouteAuthExcluded(route.Method, path), + "unexpected auth-exclusion for sandbox route %s", + key, + ) + } - assert.False(t, isPublic, "sandbox route %s should be auth-protected", key) + for expectedRoute := range publicSandboxRoutes { + assert.Containsf( + t, + discoveredSandboxRoutes, + expectedRoute, + "expected whitelisted sandbox route %s to stay registered", + expectedRoute, + ) } } @@ -74,3 +82,10 @@ func concreteSandboxRoute(path string) string { } return strings.Join(parts, "/") } + +func isSandboxRouteAuthExcluded(method, path string) bool { + w := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(w) + ctx.Request = httptest.NewRequest(method, path, nil) + return middleware.IsAuthExcluded(ctx) +}