Tenant isolation breach due to missing apiserver context propagation to health checks

MEDIUM
grafana/grafana
Commit: db0872b34fcf
Affected: before 12.4.0 (<= 12.3.x)
2026-07-01 10:45 UTC

Description

The health check subresource did not propagate the apiserver request context to the health-check path. It used req.Context() to derive the tracing span and health context, which lacks apiserver-request-scoped values such as the NamespaceValue(ctx). As a result, tenant isolation could be violated: health checks for a datasource could execute under an empty or incorrect namespace, potentially leaking or mixing tenant data across requests. The patch propagates the outer apiserver context (ctx) into the health check path, ensuring the namespace is preserved for proper multi-tenant isolation.

Proof of Concept

PoC (reproducible against pre-fix behavior; demonstrates that the apiserver namespace is not propagated to the health check before the fix): // Minimal reproduction leveraging the test-style scaffolding from the commit package main import ( "context" "net/http" "net/http/httptest" "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/config" "k8s.io/apiserver/pkg/endpoints/request" ) func TestHealthCheckNamespacePropagation_PreFixLike(t *testing.T) { var gotNamespace string shr := subHealthREST{ builder: &DataSourceAPIBuilder{ client: mockHealthClient{ checkHealthFunc: func(ctx context.Context, _ *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { // capture the namespace from the health check's context gotNamespace = request.NamespaceValue(ctx) return &backend.CheckHealthResult{Status: backend.HealthStatusOk, Message: "ok"}, nil }, }, datasources: &mockHealthDatasourceProvider{instanceSettings: &backend.DataSourceInstanceSettings{}}, contextProvider: &mockHealthContextProvider{pluginCtx: backend.PluginContext{GrafanaConfig: config.NewGrafanaCfg(map[string]string{})}}, }, } // apiserver context with a namespace, which should reach the plugin health check ctx := request.WithNamespace(context.Background(), "stacks-10782") responder := &mockHealthResponder{} handler, err := shr.Connect(ctx, "dsname", nil, responder) if err != nil { t.Fatalf("connect error: %v", err) } handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil)) if gotNamespace != "stacks-10782" { t.Fatalf("expected namespace to be propagated to health check: got=%q want=%q", gotNamespace, "stacks-10782") } } // Note: This PoC mirrors the propagation check that the patch fixed; in pre-fix behavior, gotNamespace would be empty.

Commit Details

Author: Andres Martinez Gotor

Date: 2026-07-01 09:12 UTC

Message:

datasource: propagate the apiserver request context to the health check (#127621) The health subresource built the context for the plugin CheckHealth call from the connect handler's req.Context() instead of the apiserver request context (ctx) passed to Connect. Request-scoped values set by the apiserver - notably the request namespace via request.NamespaceValue(ctx) - were therefore unavailable to the plugin client on the health path, unlike the query subresource which threads ctx through to the plugin client. Derive the request span and the CheckHealth context from ctx, matching the query subresource.

Triage Assessment

Vulnerability Type: Information disclosure / Privilege escalation

Confidence: MEDIUM

Reasoning:

The commit ensures the apiserver request context (including the request namespace) is propagated to the health check path, ensuring tenant isolation is correctly enforced for health checks. Previously, the health subresource used the inner request context, which could fail to carry namespace information and potentially leak or mix tenant data across requests. This change fixes context propagation to preserve proper security boundaries (tenant isolation) when evaluating health checks.

Verification Assessment

Vulnerability Type: Tenant isolation breach due to missing apiserver context propagation to health checks

Confidence: MEDIUM

Affected Versions: before 12.4.0 (<= 12.3.x)

Code Diff

diff --git a/pkg/registry/apis/datasource/sub_health.go b/pkg/registry/apis/datasource/sub_health.go index ae430b27609d2..0e44dd8ccf2d1 100644 --- a/pkg/registry/apis/datasource/sub_health.go +++ b/pkg/registry/apis/datasource/sub_health.go @@ -78,7 +78,7 @@ func (r *subHealthREST) Connect(ctx context.Context, name string, opts runtime.O return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { defer m.Record() - _, reqSpan := tracing.Start(req.Context(), "datasource.health.request", + _, reqSpan := tracing.Start(ctx, "datasource.health.request", attribute.String("namespace", namespace), attribute.String("plugin_id", r.builder.pluginJSON.ID), attribute.String("datasource_uid", name), @@ -102,7 +102,7 @@ func (r *subHealthREST) Connect(ctx context.Context, name string, opts runtime.O return } - healthCtx := config.WithGrafanaConfig(req.Context(), pluginCtx.GrafanaConfig) + healthCtx := config.WithGrafanaConfig(ctx, pluginCtx.GrafanaConfig) healthCtx = contextualMiddlewares(healthCtx) checkHealthCtx, checkHealthSpan := tracing.Start(healthCtx, "datasource.health.pluginClient.CheckHealth") diff --git a/pkg/registry/apis/datasource/sub_health_test.go b/pkg/registry/apis/datasource/sub_health_test.go index 6c10ec8258d32..0c5d4f835e7ec 100644 --- a/pkg/registry/apis/datasource/sub_health_test.go +++ b/pkg/registry/apis/datasource/sub_health_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/request" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/config" @@ -393,6 +394,36 @@ func TestSubHealthREST_HandlerMapsResponse(t *testing.T) { require.Equal(t, "Test Datasource", capturedRequest.PluginContext.DataSourceInstanceSettings.Name) }) + t.Run("CheckHealth context carries the request namespace", func(t *testing.T) { + var gotNamespace string + shr := subHealthREST{ + builder: &DataSourceAPIBuilder{ + client: mockHealthClient{ + checkHealthFunc: func(ctx context.Context, _ *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + gotNamespace = request.NamespaceValue(ctx) + return &backend.CheckHealthResult{Status: backend.HealthStatusOk, Message: "ok"}, nil + }, + }, + datasources: &mockHealthDatasourceProvider{instanceSettings: &backend.DataSourceInstanceSettings{}}, + contextProvider: &mockHealthContextProvider{pluginCtx: backend.PluginContext{GrafanaConfig: config.NewGrafanaCfg(map[string]string{})}}, + }, + } + + // The apiserver puts the request namespace in the Connect context. It must reach the + // plugin CheckHealth call so the multi-tenant middleware can resolve stack settings; + // the inner req.Context() does not carry it (httptest requests use context.Background). + ctx := request.WithNamespace(context.Background(), "stacks-10782") + responder := &mockHealthResponder{} + handler, err := shr.Connect(ctx, "dsname", nil, responder) + require.NoError(t, err) + + handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil)) + + require.NoError(t, responder.err) + require.Equal(t, "stacks-10782", gotNamespace, + "the namespace from the apiserver request context must reach the plugin CheckHealth call") + }) + t.Run("handler succeeds with full request URL", func(t *testing.T) { shr := subHealthREST{ builder: &DataSourceAPIBuilder{
← Back to Alerts View on GitHub →