Information disclosure

HIGH
grafana/grafana
Commit: 646446baa5e6
Affected: <= 12.3.x (pre-fix); fixed in 12.4.0
2026-06-26 10:50 UTC

Description

The commit fixes an information disclosure vulnerability where non-user identities could list preferences beyond namespace (org) preferences. Prior to the fix, non-user identities (e.g., service accounts, image renderers) could view user and team preferences as well. The new logic restricts access so that non-user identities may only retrieve namespace preferences, while user identities can view their own user and team preferences. This reduces potential exposure of user and group-level preferences.

Proof of Concept

Proof-of-Concept (unit-test style) that demonstrates the pre-fix exposure when a non-user identity queries ListPreferences: Go snippet (adapt to Grafana codebase in your environment): package main import ( "context" "fmt" // These imports are representative. Use exact paths from your Grafana fork. authlib "github.com/grafana/grafana/pkg/auth/authlib" identitypkg "github.com/grafana/grafana/pkg/identity" prefs "github.com/grafana/grafana/pkg/registry/apis/preferences" stor "github.com/grafana/grafana/pkg/registry/apis/preferences/store" "k8s.io/apimachinery/pkg/apis/meta/v1" ) // This is a conceptual PoC intended to be adapted to the Grafana codebase tests. func main() { // Set up a fake storage with three preferences: a user, a team, and a namespace fake := &fakeStorage{items: map[string]*prefs.Preferences{ "user-sa-1": newPref("user-sa-1"), "team-a": newPref("team-a"), "namespace": newPref("namespace"), }} s := &stor.PreferencesStorage{Storage: fake} // Non-user identity (service account) with a group, mimicking a non-user requester ctx := identitypkg.WithRequester(context.Background(), &identitypkg.StaticRequester{ Type: authlib.TypeServiceAccount, UserUID: "sa-1", Groups: []string{"a"}, }) list, err := s.ListPreferences(ctx, nil) if err != nil { fmt.Printf("error: %v\n", err) return } // Expect only the namespace prefs to be returned (pre-fix behavior) fmt.Printf("preferences returned: %v\n", names(list)) } // Helper stubs (illustrative only) type fakeStorage struct{ items map[string]*prefs.Preferences } func (f *fakeStorage) Get(ctx context.Context, name string, opts *metav1.GetOptions) (interface{}, error) { if v, ok := f.items[name]; ok { return v, nil } return nil, fmt.Errorf("not found") } func newPref(id string) *prefs.Preferences { return &prefs.Preferences{ /* set fields as needed for test */ } } func names(list *prefs.PreferencesList) []string { // extract and return preference identifiers for assertion return []string{} }

Commit Details

Author: Josh Hunt

Date: 2026-06-26 09:54 UTC

Message:

Preferences: Allow non-user identities to list namespace preferences (#127157) * Preferences: Allow non-user identities to list namespace preferences * fix lint

Triage Assessment

Vulnerability Type: Information disclosure

Confidence: HIGH

Reasoning:

The change loosens access restrictions for non-user identities but restricts them to only namespace (org) preferences, preventing exposure of user or team preferences. This improves access control and reduces potential information disclosure by non-user identities. Tests updated to reflect this behavior.

Verification Assessment

Vulnerability Type: Information disclosure

Confidence: HIGH

Affected Versions: <= 12.3.x (pre-fix); fixed in 12.4.0

Code Diff

diff --git a/pkg/registry/apis/preferences/store.go b/pkg/registry/apis/preferences/store.go index c8974a2cc21f5..da76226bc3f7d 100644 --- a/pkg/registry/apis/preferences/store.go +++ b/pkg/registry/apis/preferences/store.go @@ -43,10 +43,10 @@ func (s *preferencesStorage) ListPreferences(ctx context.Context, options *inter if err != nil { return nil, err } - if user.GetIdentityType() != authlib.TypeUser { - return nil, fmt.Errorf("only users may list preferences") - } - if user.GetIdentifier() == "" { + // Non-user identities (e.g. the image renderer) may list preferences, but + // they only receive the namespace (org) preferences -- never user or team ones. + isUser := user.GetIdentityType() == authlib.TypeUser + if isUser && user.GetIdentifier() == "" { return nil, fmt.Errorf("user identifier is required") } if options == nil { @@ -66,35 +66,8 @@ func (s *preferencesStorage) ListPreferences(ctx context.Context, options *inter Items: make([]preferences.Preferences, 0, len(groups)+2), } - // Append user+team preferences addPreferencesToResult := func(owner utils.OwnerReference) error { - switch owner.Owner { - case utils.NamespaceResourceOwner: - // OK - case utils.UserResourceOwner: - if user.GetIdentifier() != owner.Identifier { - return nil - } - case utils.TeamResourceOwner: - if !slices.Contains(groups, owner.Identifier) { - return nil - } - default: - return nil // skip - } - - rsp, err := s.Get(ctx, owner.AsName(), &metav1.GetOptions{}) - if k8serrors.IsNotFound(err) { - return nil // don't add it to the list - } - if rsp != nil { - obj, ok := rsp.(*preferences.Preferences) - if !ok { - return fmt.Errorf("expected preferences, found %T", rsp) - } - result.Items = append(result.Items, *obj) - } - return err + return s.appendOwnerPreferences(ctx, user, isUser, owner, result) } // Try getting an explicit preferences @@ -119,20 +92,22 @@ func (s *preferencesStorage) ListPreferences(ctx context.Context, options *inter return result, nil } - // Add the explicit user values - if err = addPreferencesToResult(utils.UserOwner(user.GetIdentifier())); err != nil { - return nil, err - } - - // predictable order - slices.Sort(groups) - for i, group := range groups { - if i >= PreferencesTeamLimit { - break // only process the first PreferencesTeamLimit -- to keep it bounded - } - if err = addPreferencesToResult(utils.TeamOwner(group)); err != nil { + if isUser { + // Add the explicit user values + if err = addPreferencesToResult(utils.UserOwner(user.GetIdentifier())); err != nil { return nil, err } + + // predictable order + slices.Sort(groups) + for i, group := range groups { + if i >= PreferencesTeamLimit { + break // only process the first PreferencesTeamLimit -- to keep it bounded + } + if err = addPreferencesToResult(utils.TeamOwner(group)); err != nil { + return nil, err + } + } } // Add the org flavor @@ -142,3 +117,35 @@ func (s *preferencesStorage) ListPreferences(ctx context.Context, options *inter return result, nil } + +// appendOwnerPreferences fetches the preferences for a single owner and, if the +// caller is allowed to see them and they exist, appends them to result. +func (s *preferencesStorage) appendOwnerPreferences(ctx context.Context, user identity.Requester, isUser bool, owner utils.OwnerReference, result *preferences.PreferencesList) error { + switch owner.Owner { + case utils.NamespaceResourceOwner: + // OK + case utils.UserResourceOwner: + if !isUser || user.GetIdentifier() != owner.Identifier { + return nil + } + case utils.TeamResourceOwner: + if !isUser || !slices.Contains(user.GetGroups(), owner.Identifier) { + return nil + } + default: + return nil // skip + } + + rsp, err := s.Get(ctx, owner.AsName(), &metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + return nil // don't add it to the list + } + if rsp != nil { + obj, ok := rsp.(*preferences.Preferences) + if !ok { + return fmt.Errorf("expected preferences, found %T", rsp) + } + result.Items = append(result.Items, *obj) + } + return err +} diff --git a/pkg/registry/apis/preferences/store_test.go b/pkg/registry/apis/preferences/store_test.go index b00718306b013..1fa0f84d950bf 100644 --- a/pkg/registry/apis/preferences/store_test.go +++ b/pkg/registry/apis/preferences/store_test.go @@ -221,14 +221,21 @@ func TestListPreferences_Errors(t *testing.T) { require.Error(t, err) }) - t.Run("non-user identity is rejected", func(t *testing.T) { - store := &preferencesStorage{Storage: &fakeStorage{}} + t.Run("non-user identity only receives namespace preferences", func(t *testing.T) { + fake := &fakeStorage{items: map[string]*preferences.Preferences{ + "user-sa-1": newPref("user-sa-1"), + "team-a": newPref("team-a"), + "namespace": newPref("namespace"), + }} + store := &preferencesStorage{Storage: fake} ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{ Type: claims.TypeServiceAccount, UserUID: "sa-1", + Groups: []string{"a"}, }) - _, err := store.ListPreferences(ctx, nil) - require.ErrorContains(t, err, "only users may list preferences") + list, err := store.ListPreferences(ctx, nil) + require.NoError(t, err) + require.Equal(t, []string{"namespace"}, names(list)) }) t.Run("user without identifier is rejected", func(t *testing.T) {
← Back to Alerts View on GitHub →