Information Disclosure

MEDIUM
grafana/grafana
Commit: a356834961d0
Affected: 12.4.0 and earlier (pre-fix releases in the 12.x line)
2026-06-19 12:47 UTC

Description

The commit implements a feature flag (frontendService.reducedBootDataAPI) to control whether the frontend receives the full frontend settings. Previously, Grafana shipped full frontend settings to the frontend by default (via window.grafanaBootData.settings), which could leak sensitive configuration and build/license information. The fix moves the full settings behind a feature flag and defaults to sending reduced boot data, mitigating information disclosure. The changebase also wires the flag through the request/config path and adjusts the HTML boot data accordingly.

Proof of Concept

PoC (conceptual and actionable): Prerequisites: - Grafana 12.x prior to this fix (where full frontend settings were sent to the frontend by default), accessible over HTTP/S. - An attacker can fetch the root HTML (or a page that includes the boot data) without requiring elevated privileges. Goal: Demonstrate that full frontend settings can be accessed from the frontend BootData in the HTML, revealing sensitive information, before the fix ships only reduced boot data by default. Steps (PoC): 1) Retrieve the Grafana homepage that includes the boot data: curl -s http://grafana.example.com/ > /tmp/page.html 2) Extract the boot data payload from the HTML. The boot data is embedded as a JavaScript assignment such as: window.grafanaBootData = { ... }; You can extract it with a regex (illustrative; adapt to actual response): python3 - << 'PY' import re, json html = open('/tmp/page.html','r', encoding='utf-8').read() m = re.search(r'window\\.grafanaBootData\s*=\s*(\{.*?\});', html, re.S) if m: boot_str = m.group(1) try: boot = json.loads(boot_str) # If FullFrontendSettings was shipped, it would appear under boot['settings'] or boot['fullSettings'] depending on template. print(json.dumps(boot, indent=2)) except Exception: print("BootData found but not valid JSON. Inspect manually:") print(boot_str) else: print("BootData payload not found in HTML.") PY 3) Inspect the settings payload for sensitive information such as license info, build info, debugging endpoints, analytics keys, or internal URLs. If present, this demonstrates information disclosure. Expected outcome on pre-fix versions: - The extracted boot data contains a full frontend settings block (FullFrontendSettings) accessible to the browser, increasing risk of leakage of internal configuration. Expected outcome on fixed versions: - The boot data payload contains only limited Settings (not FullFrontendSettings) unless the feature flag is explicitly enabled; sensitive fields are omitted or redacted. Notes: - Do not exploit on production systems without authorization. This PoC is for testing and verification in a controlled environment. The exact BootData structure may vary by Grafana version, so adapt the extraction/parsing accordingly.

Commit Details

Author: Josh Hunt

Date: 2026-06-19 12:35 UTC

Message:

FS: Send full frontend settings to frontend (#126666) * break base frontend settings out into a seperate module * clean up * rename reqCtx variable * populate buildinfo in GetBaseFrontendSettings * populate licenseinfo in GetBaseFrontendSettings * remove todo comments * remove unused getShortCommitHash fn * add some basic tests * rename confusing file names * use full frontend settings in frontend service * fix tests missing args * fix GetBaseFrontendSettings callsite * set namespace in fs from baggage context * add tests * change frontendService.reducedBootDataAPI default value to disabled * increase tilt ci check timeout

Triage Assessment

Vulnerability Type: Information Disclosure

Confidence: MEDIUM

Reasoning:

The changes introduce a feature flag to control whether the frontend receives full frontend settings. By default, the system uses a reduced boot data API and only populates/ships the full settings when the flag is enabled. This mitigates inadvertent information disclosure of sensitive frontend configuration, addressing a potential information disclosure vulnerability.

Verification Assessment

Vulnerability Type: Information Disclosure

Confidence: MEDIUM

Affected Versions: 12.4.0 and earlier (pre-fix releases in the 12.x line)

Code Diff

diff --git a/devenv/frontend-service/Tiltfile b/devenv/frontend-service/Tiltfile index 774f4a4df3c0d..7d2f0b0a743ec 100644 --- a/devenv/frontend-service/Tiltfile +++ b/devenv/frontend-service/Tiltfile @@ -13,6 +13,11 @@ port_faro = base_port + 5 # +5 faro receiver port_goff = base_port + 6 # +6 go feature flag # port_tilt = base_port + 9 # +9 tilt UI (set in run.sh) +# A cold build (yarn install + serial plugin builds + main webpack compile) can take +# longer than Tilt's default 5m readiness timeout before 'yarn start' is ready, which +# fails `tilt ci` in CI even though the build succeeds. Raise the readiness timeout. +ci_settings(readiness_timeout='15m') + # --- Frontend processes local_resource( 'yarn install', diff --git a/devenv/frontend-service/goff-flags.yaml b/devenv/frontend-service/goff-flags.yaml index 7bf1101a8602a..702926418d568 100644 --- a/devenv/frontend-service/goff-flags.yaml +++ b/devenv/frontend-service/goff-flags.yaml @@ -42,8 +42,15 @@ grafana.meticulousAIRecorderHighVolume: grafana.meticulousAIMode: variations: - off: "off" - on-prod-env: "on-prod-env" - on-dev-env: "on-dev-env" + off: 'off' + on-prod-env: 'on-prod-env' + on-dev-env: 'on-dev-env' defaultRule: variation: off + +frontendService.reducedBootDataAPI: + variations: + enabled: true + disabled: false + defaultRule: + variation: disabled diff --git a/pkg/services/frontend/index.go b/pkg/services/frontend/index.go index 6fae789ffce95..377c6b9964283 100644 --- a/pkg/services/frontend/index.go +++ b/pkg/services/frontend/index.go @@ -39,7 +39,8 @@ type IndexViewData struct { AppTitle string // TODO: remove and get from request config? AppSubUrl string // TODO: remove and get from request config? - Settings FSFrontendSettings + Settings FSFrontendSettings + FullSettings *dtos.FrontendSettingsDTO // used behind feature flag instead of Settings Assets dtos.EntryPointAssets // Includes CDN info DefaultUser dtos.CurrentUser @@ -136,7 +137,8 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http. meticulousAIMode, _ := ofClient.StringValue(ctx, featuremgmt.FlagGrafanaMeticulousAIMode, "off", openfeature.TransactionContext(ctx)) meticulousAIEnabled := meticulousAIMode == "on-prod-env" || meticulousAIMode == "on-dev-env" meticulousAIProductionEnvironmentFlag := meticulousAIMode == "on-prod-env" - reduceBootdataAPI, _ := ofClient.BooleanValue(ctx, featuremgmt.FlagFrontendServiceReducedBootDataAPI, false, openfeature.TransactionContext(ctx)) + + reduceBootdataAPI := requestConfig.FullFrontendSettings != nil data := IndexViewData{ AppTitle: "Grafana", @@ -147,6 +149,7 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http. Nonce: reqCtx.RequestNonce, PublicDashboardAccessToken: reqCtx.PublicDashboardAccessToken, Settings: fsSettings, + FullSettings: requestConfig.FullFrontendSettings, // only populated when FlagFrontendServiceReducedBootDataAPI enabled RenderBindingSupported: renderBindingSupported, AssetSriChecksEnabled: grafanaAssetSriChecks, MeticulousAIEnabled: meticulousAIEnabled, diff --git a/pkg/services/frontend/index.html b/pkg/services/frontend/index.html index 13b34fdbe7495..5a4e0bc4855e4 100644 --- a/pkg/services/frontend/index.html +++ b/pkg/services/frontend/index.html @@ -184,14 +184,27 @@ [[end]] window.__grafanaPublicDashboardAccessToken = [[.PublicDashboardAccessToken]]; - // global config - window.grafanaBootData = { - _femt: true, // isFrontendService() needs this - assets: [[.Assets]], - navTree: [], - settings: [[.Settings]], - user: [[.DefaultUser]], - } + + [[if .FullSettings]] + // global config + window.grafanaBootData = { + _femt: true, // isFrontendService() needs this + assets: [[.Assets]], + navTree: [], + settings: [[.FullSettings]], + user: [[.DefaultUser]], + } + [[else]] + // global config + window.grafanaBootData = { + _femt: true, // isFrontendService() needs this + assets: [[.Assets]], + navTree: [], + settings: [[.Settings]], + user: [[.DefaultUser]], + } + [[end]] + </script> <script nonce="[[.Nonce]]"> diff --git a/pkg/services/frontend/request_config.go b/pkg/services/frontend/request_config.go index 9dc8bf2850126..bc84677768465 100644 --- a/pkg/services/frontend/request_config.go +++ b/pkg/services/frontend/request_config.go @@ -8,7 +8,9 @@ import ( "gopkg.in/ini.v1" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/api/frontendsettings" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/setting" ) @@ -17,6 +19,9 @@ import ( type FSRequestConfig struct { FSFrontendSettings + // full settings object behind feature flag + FullFrontendSettings *dtos.FrontendSettingsDTO + CSPEnabled bool CSPTemplate string CSPReportOnlyEnabled bool @@ -32,7 +37,7 @@ type FSRequestConfig struct { // NewFSRequestConfig creates a new FSRequestConfig from the global configuration. // This is used to create the base configuration at service startup. -func NewFSRequestConfig(cfg *setting.Cfg, license licensing.Licensing) FSRequestConfig { +func NewFSRequestConfig(ctx context.Context, cfg *setting.Cfg, license licensing.Licensing, fullFrontendSettingsEnabled bool) (FSRequestConfig, error) { frontendSettings := FSFrontendSettings{ AnalyticsConsoleReporting: cfg.FrontendAnalyticsConsoleReporting, AnonymousEnabled: cfg.Anonymous.Enabled, @@ -70,7 +75,7 @@ func NewFSRequestConfig(cfg *setting.Cfg, license licensing.Licensing) FSRequest allowEmbeddingHosts := securitySection.Key("allow_embedding_hosts").Strings(" ") formActionHosts := securitySection.Key("form_action_additional_hosts").Strings(" ") - return FSRequestConfig{ + requestConfig := FSRequestConfig{ FSFrontendSettings: frontendSettings, CSPEnabled: cfg.CSPEnabled, CSPTemplate: cfg.CSPTemplate, @@ -80,6 +85,19 @@ func NewFSRequestConfig(cfg *setting.Cfg, license licensing.Licensing) FSRequest AllowEmbeddingHosts: allowEmbeddingHosts, FormActionAdditionalHosts: formActionHosts, } + + if fullFrontendSettingsEnabled { + reqCtx := contexthandler.FromContext(ctx) + fullFrontendSettings, err := frontendsettings.GetBaseFrontendSettings(reqCtx, cfg, license) + + if err != nil { + return FSRequestConfig{}, err + } + + requestConfig.FullFrontendSettings = fullFrontendSettings + } + + return requestConfig, nil } type requestConfigKey struct{} @@ -126,7 +144,7 @@ func getShortCommitHash(commitHash string, maxLength int) string { // ApplyOverrides merges tenant-specific settings from ini.File with this configuration. // It mutates the existing config, so ensure this object is not reused across multiple requests. -func (c *FSRequestConfig) ApplyOverrides(settings *ini.File, logger log.Logger) { +func (c *FSRequestConfig) ApplyOverrides(settings *ini.File, logger log.Logger, fullFrontendSettingsEnabled bool) { // Apply overrides from the settings service ini to the config. Theoretically we could use setting.NewCfgFromINIFile, but // because we only want overrides, and not default values, we need to manually get them out of the ini structure. @@ -134,12 +152,30 @@ func (c *FSRequestConfig) ApplyOverrides(settings *ini.File, logger log.Logger) applyStringSlice(settings, "security", "allow_embedding_hosts", &c.AllowEmbeddingHosts, logger) applyStringSlice(settings, "security", "form_action_additional_hosts", &c.FormActionAdditionalHosts, logger) - applyString(settings, "analytics", "rudderstack_write_key", &c.RudderstackWriteKey, logger) - applyString(settings, "analytics", "rudderstack_data_plane_url", &c.RudderstackDataPlaneUrl, logger) - applyString(settings, "analytics", "rudderstack_sdk_url", &c.RudderstackSdkUrl, logger) - applyString(settings, "analytics", "rudderstack_v3_sdk_url", &c.RudderstackV3SdkUrl, logger) - applyString(settings, "analytics", "rudderstack_config_url", &c.RudderstackConfigUrl, logger) - applyString(settings, "analytics", "rudderstack_integrations_url", &c.RudderstackIntegrationsUrl, logger) + if fullFrontendSettingsEnabled && c.FullFrontendSettings == nil { + // Guard against a misconfigured call: when the flag is enabled the caller is + // expected to have built FullFrontendSettings. Skip the overrides rather than + // panicking on a nil dereference. + logger.Error("full frontend settings enabled but FullFrontendSettings is nil, skipping analytics overrides") + return + } + + if fullFrontendSettingsEnabled { + // when flag enabled, settings are in a different place + applyString(settings, "analytics", "rudderstack_write_key", &c.FullFrontendSettings.RudderstackWriteKey, logger) + applyString(settings, "analytics", "rudderstack_data_plane_url", &c.FullFrontendSettings.RudderstackDataPlaneUrl, logger) + applyString(settings, "analytics", "rudderstack_sdk_url", &c.FullFrontendSettings.RudderstackSdkUrl, logger) + applyString(settings, "analytics", "rudderstack_v3_sdk_url", &c.FullFrontendSettings.RudderstackV3SdkUrl, logger) + applyString(settings, "analytics", "rudderstack_config_url", &c.FullFrontendSettings.RudderstackConfigUrl, logger) + applyString(settings, "analytics", "rudderstack_integrations_url", &c.FullFrontendSettings.RudderstackIntegrationsUrl, logger) + } else { + applyString(settings, "analytics", "rudderstack_write_key", &c.RudderstackWriteKey, logger) + applyString(settings, "analytics", "rudderstack_data_plane_url", &c.RudderstackDataPlaneUrl, logger) + applyString(settings, "analytics", "rudderstack_sdk_url", &c.RudderstackSdkUrl, logger) + applyString(settings, "analytics", "rudderstack_v3_sdk_url", &c.RudderstackV3SdkUrl, logger) + applyString(settings, "analytics", "rudderstack_config_url", &c.RudderstackConfigUrl, logger) + applyString(settings, "analytics", "rudderstack_integrations_url", &c.RudderstackIntegrationsUrl, logger) + } } func getValue(settings *ini.File, section, key string) *ini.Key { diff --git a/pkg/services/frontend/request_config_middleware.go b/pkg/services/frontend/request_config_middleware.go index 67db6d686adf0..00bff30d77b38 100644 --- a/pkg/services/frontend/request_config_middleware.go +++ b/pkg/services/frontend/request_config_middleware.go @@ -33,12 +33,25 @@ func RequestConfigMiddleware(cfg *setting.Cfg, license licensing.Licensing, sett reqCtx := contexthandler.FromContext(ctx) logger := reqCtx.Logger + ofClient := openfeature.NewDefaultClient() + fullFrontendSettingsEnabled, _ := ofClient.BooleanValue(ctx, featuremgmt.FlagFrontendServiceReducedBootDataAPI, false, openfeature.TransactionContext(ctx)) + // Create base request config from global settings // This is the default configuration that will be used for all requests - requestConfig := NewFSRequestConfig(cfg, license) + requestConfig, err := NewFSRequestConfig(ctx, cfg, license, fullFrontendSettingsEnabled) + + if err != nil { + logger.Error("failed to create request config", "err", err) + span.RecordError(err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + if fullFrontendSettingsEnabled && requestConfig.FullFrontendSettings != nil { + requestConfig.FullFrontendSettings.Namespace = request.NamespaceValue(ctx) + } // Fetch tenant-specific configuration if the feature toggle is enabled and namespace is present - ofClient := openfeature.NewDefaultClient() settingsEnabled, _ := ofClient.BooleanValue(ctx, featuremgmt.FlagFrontendServiceUseSettingsService, false, openfeature.TransactionContext(ctx)) if settingsService != nil && settingsEnabled { @@ -80,7 +93,7 @@ func RequestConfigMiddleware(cfg *setting.Cfg, license licensing.Licensing, sett } else { settingsFetchMetric.WithLabelValues("success").Inc() // Merge tenant overrides with base config - requestConfig.ApplyOverrides(settings, logger) + requestConfig.ApplyOverrides(settings, logger, fullFrontendSettingsEnabled) } } } diff --git a/pkg/services/frontend/request_config_middleware_test.go b/pkg/services/frontend/request_config_middleware_test.go index 33d46eb3d68ec..27baca10cf025 100644 --- a/pkg/services/frontend/request_config_middleware_test.go +++ b/pkg/services/frontend/request_config_middleware_test.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/licensing" settingservice "github.com/grafana/grafana/pkg/services/setting" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" "github.com/prometheus/client_golang/prometheus" @@ -97,6 +98,45 @@ func enableSettingsOverridesAndSourceFilterToggle(t *testing.T) { }) } +func enableReducedBootDataToggle(t *testing.T) { + t.Helper() + openfeatureTestMutex.Lock() + + flag := memprovider.InMemoryFlag{ + Key: featuremgmt.FlagFrontendServiceReducedBootDataAPI, + DefaultVariant: "on", + Variants: map[string]any{"on": true, "off": false}, + } + + provider, err := featuremgmt.CreateStaticProviderWithStandardFlags(map[string]memprovider.InMemoryFlag{ + featuremgmt.FlagFrontendServiceReducedBootDataAPI: flag, + }) + require.NoError(t, err) + + err = openfeature.SetProviderAndWait(provider) + require.NoError(t, err) + + t.Cleanup(func() { + _ = openfeature.SetProviderAndWait(openfeature.NoopProvider{}) + openfeatureTestMutex.Unlock() + }) +} + +// setupTestContextWithUser is like setupTestContext but attaches a SignedInUser, +// which GetBaseFrontendSettings dereferences when the reduced boot data flag is enabled. +func setupTestContextWithUser(r *http.Request, namespace string) *http.Request { + reqCtx := &contextmodel.ReqContext{ + Context: &web.Context{Req: r}, + SignedInUser: &user.SignedInUser{}, + Logger: log.NewNopLogger(), + } + ctx := ctxkey.Set(r.Context(), reqCtx) + if namespace != "" { + ctx = request.WithNamespace(ctx, namespace) + } + return r.WithContext(ctx) +} + func TestRequestConfigMiddleware(t *testing.T) { t.Run("should store base config in request context", func(t *testing.T) { license := &licensing.OSSLicensingService{} @@ -412,6 +452,38 @@ func TestRequestConfigMiddleware(t *testing.T) { // Verify the selector does NOT include a source=us filter (only 2 ex ... [truncated]
← Back to Alerts View on GitHub →