Просмотр исходного кода

fix(security): require admin auth for GET /helmValues

The /helmValues endpoint returned the base64-decoded HELM_VALUES
environment variable to any unauthenticated caller. In a Helm deploy
this can include cloud provider and billing credentials, making it an
unauthenticated information disclosure (issue #3893 /
GHSA-vm98-22x4-xwvv).

Wrap the route in adminAuthMiddleware, consistent with the adjacent
admin-protected routes such as POST /serviceKey, so that when
ADMIN_TOKEN is configured a valid Bearer token is required. Adds a
test verifying unauthenticated or wrongly authenticated requests are
rejected without leaking the Helm values, and correctly authenticated
requests succeed.

Closes #3893

Signed-off-by: Warwick <warwick@automatic.systems>
Claude 2 месяцев назад
Родитель
Сommit
9e14af5e3c
2 измененных файлов с 79 добавлено и 1 удалено
  1. 1 1
      pkg/costmodel/router.go
  2. 78 0
      pkg/costmodel/router_test.go

+ 1 - 1
pkg/costmodel/router.go

@@ -591,7 +591,7 @@ func Initialize(router *httprouter.Router, additionalConfigWatchers ...*watcher.
 	router.GET("/installNamespace", a.GetInstallNamespace)
 	router.GET("/installInfo", a.GetInstallInfo)
 	router.POST("/serviceKey", adminAuthMiddleware(a.AddServiceKey))
-	router.GET("/helmValues", a.GetHelmValues)
+	router.GET("/helmValues", adminAuthMiddleware(a.GetHelmValues))
 
 	return a
 }

+ 78 - 0
pkg/costmodel/router_test.go

@@ -1,9 +1,11 @@
 package costmodel
 
 import (
+	"encoding/base64"
 	"net/http"
 	"net/http/httptest"
 	"os"
+	"strings"
 	"testing"
 
 	"github.com/julienschmidt/httprouter"
@@ -104,3 +106,79 @@ func TestAdminAuthMiddleware(t *testing.T) {
 		})
 	}
 }
+
+// TestHelmValuesRequiresAdminAuth verifies that the /helmValues handler, wrapped in
+// adminAuthMiddleware exactly as registered in Initialize, rejects unauthenticated
+// requests without leaking the Helm values (issue #3893 / GHSA-vm98-22x4-xwvv).
+func TestHelmValuesRequiresAdminAuth(t *testing.T) {
+	const testToken = "test-admin-token-123"
+	const helmValues = "cloudSecret: super-secret-value"
+
+	tests := []struct {
+		name       string
+		authHeader string
+		wantStatus int
+		wantValues bool
+	}{
+		{
+			name:       "missing authorization header",
+			authHeader: "",
+			wantStatus: http.StatusUnauthorized,
+			wantValues: false,
+		},
+		{
+			name:       "bearer with wrong token",
+			authHeader: "Bearer wrong-token",
+			wantStatus: http.StatusForbidden,
+			wantValues: false,
+		},
+		{
+			name:       "bearer with correct token",
+			authHeader: "Bearer " + testToken,
+			wantStatus: http.StatusOK,
+			wantValues: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			prevToken := os.Getenv(env.AdminTokenEnvVar)
+			defer func() {
+				if prevToken == "" {
+					os.Unsetenv(env.AdminTokenEnvVar)
+				} else {
+					os.Setenv(env.AdminTokenEnvVar, prevToken)
+				}
+			}()
+			os.Setenv(env.AdminTokenEnvVar, testToken)
+
+			prevHelmValues := os.Getenv("HELM_VALUES")
+			defer func() {
+				if prevHelmValues == "" {
+					os.Unsetenv("HELM_VALUES")
+				} else {
+					os.Setenv("HELM_VALUES", prevHelmValues)
+				}
+			}()
+			os.Setenv("HELM_VALUES", base64.StdEncoding.EncodeToString([]byte(helmValues)))
+
+			req := httptest.NewRequest(http.MethodGet, "/helmValues", nil)
+			if tt.authHeader != "" {
+				req.Header.Set("Authorization", tt.authHeader)
+			}
+			rec := httptest.NewRecorder()
+
+			// Wrap the handler exactly as the route registration does in Initialize.
+			a := &Accesses{}
+			handler := adminAuthMiddleware(a.GetHelmValues)
+			handler(rec, req, httprouter.Params{})
+
+			if rec.Code != tt.wantStatus {
+				t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus)
+			}
+			gotValues := strings.Contains(rec.Body.String(), helmValues)
+			if gotValues != tt.wantValues {
+				t.Errorf("response contains helm values = %v, want %v (body: %q)", gotValues, tt.wantValues, rec.Body.String())
+			}
+		})
+	}
+}