Skip to content

REPRO-2026-00227: Verified Repro With Script Download

REPRO-2026-00227: Grafana unified storage IAM service account listing lacked RBAC filtering, allowing low-privileged users to enumerate service accounts.

REPRO-2026-00227 is verified against grafana/grafana · Go. Affected versions: Versions prior to commit 8891796ca1086cd234e1715ea71d8db0073cc160 (fix adds RBAC allowlist for serviceaccounts). Fixed in Commit 8891796ca1086cd234e1715ea71d8db0073cc160. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00227.

REPRO-2026-00227 grafana/grafana · Go Jul 4, 2026 .txt
Severity
HIGH
Confidence
HIGH
Reproduced in
31m 40s
Tool calls
429
Spend
$19.28
01 · Overview

What Is REPRO-2026-00227?

REPRO-2026-00227 reproduces a high-severity RBAC bypass in Grafana's unified storage: the IAM service account listing lacked RBAC filtering, allowing low-privileged users to enumerate service accounts they were not authorized to see.

02 · Severity & CVSS

REPRO-2026-00227 Severity

REPRO-2026-00227 is rated high severity.

HIGH threat level

High — serious impact or readily exploitable. Prioritize remediation.

03 · Affected Versions

Affected grafana/grafana Versions

grafana/grafana · Go versions Versions prior to commit 8891796ca1086cd234e1715ea71d8db0073cc160 (fix adds RBAC allowlist for serviceaccounts) are affected.

How to Reproduce REPRO-2026-00227

$ pruva-verify REPRO-2026-00227
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00227/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for REPRO-2026-00227

Authorization bypass — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

Authenticated low-privileged Viewer user identity with serviceaccounts:read scoped only to alpha-sa; HTTP GET to /apis/iam.grafana.app/v0alpha1/namespaces/default/serviceaccounts

Attack chain
  1. Real Grafana unified-storage HTTP endpoint GET /apis/iam.grafana.app/v0alpha1/namespaces/{org}/serviceaccounts
  2. IAM serviceaccount List
  3. common.List
  4. authzLimitedClient.Compile for iam.grafana.app/serviceaccounts. Vulnerable allowlist omits serviceaccounts and returns an always-true item checker, exposing beta-sa; fixed allowlist delegates to RBAC and filters beta-sa.
How the agent worked 1,081 events · 429 tool calls · 32 min
32 minDuration
429Tool calls
277Reasoning steps
1,081Events
1Dead-ends
Agent activity over 32 min
Support
13
Repro
1,013
Judge
50
0:0031:40

Root Cause and Exploit Chain for REPRO-2026-00227

Versions: Builds before 8891796ca1086cd234e1715ea71d8db0073cc160 where the iam.grafana.app allowlist is {"users": nil, "teams": nil} and lacks "serviceaccounts". The reproduction anchors the vulnerable build to the fixed commit's parent, c00083433312adb7b7cfef83f74751e1216f67f8, per the fixed-commit rule.

Grafana's unified-storage authorization shim, authzLimitedClient in pkg/storage/unified/resource/access.go, only delegates authorization checks to RBAC for resources present in an internal allowlist. Before fix commit 8891796ca1086cd234e1715ea71d8db0073cc160, the iam.grafana.app allowlist covered users and teams but omitted serviceaccounts. For service account list/search/read operations, the storage layer therefore treated the resource as not RBAC-compatible and returned an allow-all list checker. In a real Grafana unified-storage HTTP server, a low-privileged user with a scoped serviceaccounts:read grant for only alpha-sa can call GET /apis/iam.grafana.app/v0alpha1/namespaces/default/serviceaccounts and receive both alpha-sa and unauthorized beta-sa on the vulnerable build; the fixed build filters the response to only alpha-sa.

  • Package/component affected: Grafana unified storage, specifically pkg/storage/unified/resource/access.go (authzLimitedClient) and the IAM service account list path backed by unified storage.
  • Affected versions: Builds before 8891796ca1086cd234e1715ea71d8db0073cc160 where the iam.grafana.app allowlist is {"users": nil, "teams": nil} and lacks "serviceaccounts". The reproduction anchors the vulnerable build to the fixed commit's parent, c00083433312adb7b7cfef83f74751e1216f67f8, per the fixed-commit rule.
  • Risk level and consequences: Authorization bypass / information disclosure. A low-privileged authenticated user who can pass the top-level list gate with a limited service-account read grant can enumerate service account objects outside the grant's scope because storage-layer per-item filtering is skipped.

Impact Parity

  • Disclosed/claimed maximum impact: authz_bypass over a remote/API surface: service account enumeration through GET /apis/iam.grafana.app/.../namespaces/{org}/serviceaccounts by a low-privileged authenticated user without permission for the enumerated service account(s).
  • Reproduced impact from this run: authz_bypass through the real Grafana HTTP/API path. The script starts a real Grafana test server with unified storage and Mode5 for serviceaccounts.iam.grafana.app, creates alpha-sa and beta-sa, creates a low-privileged Viewer user with only serviceaccounts:read scoped to alpha-sa, then performs a raw HTTP GET /apis/iam.grafana.app/v0alpha1/namespaces/default/serviceaccounts as that user. Vulnerable build returns alpha-sa and unauthorized beta-sa; fixed build returns only alpha-sa.
  • Parity: full for the product-facing API authorization-bypass class. The caller is low-privileged and lacks permission to read beta-sa, yet receives it only on the vulnerable build.
  • Not demonstrated: The repository currently serves IAM APIs as v0alpha1 only (apps/iam/pkg/apis/iam_manifest.go has PreferredVersion: "v0alpha1" and no v1 service account API). Therefore the concrete exercised path is the currently implemented service account endpoint, /apis/iam.grafana.app/v0alpha1/namespaces/{org}/serviceaccounts, matching the judge feedback's requested v0alpha1 surface rather than a non-existent v1 route in this checkout.

Root Cause

authzLimitedClient is designed as a temporary bridge that only sends certain group/resource pairs to the underlying RBAC access client. The vulnerable allowlist omits serviceaccounts:

"iam.grafana.app": map[string]interface{}{"users": nil, "teams": nil},

The affected methods first call IsCompatibleWithRBAC. If it returns false, they skip the underlying access client:

  • Check returns claims.CheckResponse{Allowed: true}.
  • Compile returns func(name, folder string) bool { return true }, so list filtering allows every object.
  • BatchCheck marks each item allowed.

The product HTTP path reaches this through the IAM service account list implementation. pkg/registry/apis/iam/serviceaccount/store.go calls common.List, and common.List calls ac.Compile(...) to obtain an item checker for the list response. In the vulnerable build, Compile returns an always-true checker for iam.grafana.app/serviceaccounts, so beta-sa is not filtered out. In the fixed build, serviceaccounts is in the allowlist, so Compile delegates to RBAC and the scoped low-privileged user only sees alpha-sa.

Fix commit:

  • 8891796ca1086cd234e1715ea71d8db0073cc160 — "Unified storage: Enforce RBAC for serviceaccount search/list/read (#127839)"

The relevant patch is the one-line allowlist addition:

-"iam.grafana.app":       map[string]interface{}{"users": nil, "teams": nil},
+"iam.grafana.app":       map[string]interface{}{"users": nil, "teams": nil, "serviceaccounts": nil},

Reproduction Steps

  1. Run bundle/repro/reproduction_steps.sh.
  2. The script:
    • Reads bundle/project_cache_context.json and reuses the stable Grafana checkout at <project_cache_dir>/repo.
    • Uses the fixed commit 8891796ca1086cd234e1715ea71d8db0073cc160 and its parent c00083433312adb7b7cfef83f74751e1216f67f8.
    • Verifies the vulnerable checkout lacks the serviceaccounts allowlist entry and the fixed checkout contains it.
    • Runs a supporting real-code library test against authzLimitedClient on both versions.
    • Compiles and runs a Grafana IAM service account integration test that starts a real Grafana HTTP server, creates service accounts/users/permissions, and issues the actual HTTP GET to the service account list endpoint as a low-privileged scoped user.
  3. Expected evidence:
    • Vulnerable HTTP run: HTTP_LIST_RESULT_JSON {"items":2,"names":["alpha-sa","beta-sa"],..."status":200} and BEHAVIOUR: VULNERABLE_HTTP.
    • Fixed HTTP run: HTTP_LIST_RESULT_JSON {"items":1,"names":["alpha-sa"],..."status":200} and BEHAVIOUR: FIXED_HTTP.
    • Script exits 0 only when both the vulnerable product exposure and fixed negative control are observed.

Evidence

  • bundle/logs/reproduction_steps.log — first successful full run.
  • bundle/logs/reproduction_steps_second.log — second consecutive successful full run.
  • bundle/logs/evidence.log — annotated evidence from the most recent script run.
  • bundle/logs/api_http_vulnerable.log — vulnerable real Grafana HTTP server attempt.
  • bundle/logs/api_http_fixed.log — fixed real Grafana HTTP server negative control.
  • bundle/logs/library_vulnerable.log and bundle/logs/library_fixed.log — supporting real-code authorization-client divergence.
  • bundle/repro/runtime_manifest.json — runtime evidence manifest written by the script.

Key first-run excerpts:

Grafana is listening on 127.0.0.1:37329
HTTP_SURFACE method=GET path=/apis/iam.grafana.app/v0alpha1/namespaces/default/serviceaccounts
LOW_PRIV_USER login=scoped-sa-reader basic_role=Viewer grant=serviceaccounts:read scoped_to_alpha_only
HTTP_LIST_RESULT_JSON {"items":2,"names":["alpha-sa","beta-sa"],"path":"/apis/iam.grafana.app/v0alpha1/namespaces/default/serviceaccounts","status":200}
BEHAVIOUR: VULNERABLE_HTTP - scoped low-priv user received unauthorized beta-sa over original HTTP serviceaccounts list endpoint

Fixed negative control from the same run:

Grafana is listening on 127.0.0.1:37211
HTTP_SURFACE method=GET path=/apis/iam.grafana.app/v0alpha1/namespaces/default/serviceaccounts
HTTP_LIST_RESULT_JSON {"items":1,"names":["alpha-sa"],"path":"/apis/iam.grafana.app/v0alpha1/namespaces/default/serviceaccounts","status":200}
BEHAVIOUR: FIXED_HTTP - scoped low-priv user list was filtered to authorized alpha-sa

Second-run confirmation:

Grafana is listening on 127.0.0.1:36869
HTTP_LIST_RESULT_JSON {"items":2,"names":["alpha-sa","beta-sa"],"path":"/apis/iam.grafana.app/v0alpha1/namespaces/default/serviceaccounts","status":200}
BEHAVIOUR: VULNERABLE_HTTP - scoped low-priv user received unauthorized beta-sa over original HTTP serviceaccounts list endpoint
...
Grafana is listening on 127.0.0.1:40001
HTTP_LIST_RESULT_JSON {"items":1,"names":["alpha-sa"],"path":"/apis/iam.grafana.app/v0alpha1/namespaces/default/serviceaccounts","status":200}
BEHAVIOUR: FIXED_HTTP - scoped low-priv user list was filtered to authorized alpha-sa

Supporting library evidence:

vulnerable allowlist: "iam.grafana.app": map[string]interface{}{"users": nil, "teams": nil}
IsCompatibleWithRBAC(iam.grafana.app, serviceaccounts) = false
Check serviceaccounts: Allowed=true
Compile serviceaccounts checker(alpha-sa) = true

fixed allowlist: "iam.grafana.app": map[string]interface{}{"users": nil, "teams": nil, "serviceaccounts": nil}
IsCompatibleWithRBAC(iam.grafana.app, serviceaccounts) = true
Check serviceaccounts: Allowed=false
Compile serviceaccounts checker(alpha-sa) = false

Environment details: Go 1.26.4, Grafana repository from the project cache, sqlite test database, Grafana test HTTP server, unified storage, serviceaccounts.iam.grafana.app dual-writer Mode5, feature flags grafanaAPIServerWithExperimentalAPIs, kubernetesServiceAccountsApi, and kubernetesServiceAccountTokensApi.

Recommendations / Next Steps

  • Apply or retain fix commit 8891796ca1086cd234e1715ea71d8db0073cc160 so serviceaccounts is included in the iam.grafana.app RBAC allowlist.
  • Add regression tests for the product HTTP list path: a low-privileged user with serviceaccounts:read scoped to one service account must not receive other service accounts from /apis/iam.grafana.app/v0alpha1/namespaces/{org}/serviceaccounts.
  • Add allowlist coverage tests so future IAM resources cannot be silently omitted from authzLimitedClient.
  • Upgrade affected Grafana deployments to a build containing the fix.

Additional Notes

  • The script is idempotent: it writes temporary Go test files, checks out only access.go at the vulnerable/fixed commits, and restores/removes modified files on exit.
  • The script was run twice consecutively and passed both times.
  • The proof uses Grafana's real HTTP server test environment and real authentication/permission setup. The library-level authzLimitedClient test is supporting evidence only; the primary proof is the product-facing HTTP endpoint divergence.
  • The implemented IAM service account API version in this repository is v0alpha1; no v1 IAM service account API is served in apps/iam/pkg/apis/iam_manifest.go. The reproduction therefore uses the original available service account HTTP route for this checkout.

REPRO-2026-00227 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:000:30
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · url-20260703-issue304 · REPRO-20
0:02
0:03
web search
0:05
web search
0:07
0:09
0:14
0:14
extract_facts
no facts extracted
0:15
0:15
0:15
supportrepro
0:19
0:19
0:21
0:21
0:21
0:21
0:22
0:22
0:24
0:24
0:25
0:25
0:25
0:25
$cd /data/pruva/project-cache/6c6f6fd2-6e61-4267-8db1-032ee6a303f9/repo && git log --oneline -5 2>/dev/null; echo "---HEAD---"; git rev-parse HEAD 2>/dev/null; echo "---BRANCH---"; git branch -a 2>/dev/null | head -20
{"cwd":"/data/pruva/runs/14a0ec96-cac6-4789-b3fb-8b167f2aecd7","duration_ms":29,"exit_code":0,"stderr":"","stdout":"a209162c2b4 feat: add nats subscriber (#127572)\n2c51b4ba57f docs: sync visualization docs with source (pt4) (#126815)\nff9c06e96f0 Alerting: always set promote header when importing Alertmanager config (#127585)\n45b5f640ea8 Secrets: add span event to EncryptionManager.currentDataKey (#127577)\n144871919b3 Provisioning: Add Github Enterprise frontend (#127209)\n---HEAD---\na209162c2b46da5bc3e39db73452c09436ecd8ff\n---BRANCH---\n* (no branch)\n  120719-alerting-docs-notification-history\n  125478-alerting-docs-edit-h2-title\n  17753-support-request---updating-an-existing-public-document---grafana---httpsgrafanacomdocsgrafanalatestalertingset-upconfigure-alert-state-historyconfigure-loki-and-prometheus-for-alert-state\n  20230331_add_kerberos_support\n  20818-support-request---updating-an-existing-public-document---alerting---httpsgrafanacomdocsgrafanalatestalertingconfigure-notificationstemplate-notificationsimages-in-notifications\n  21093-support-request---updating-an-existing-public-document---alerting---httpsgrafanacomdocsgrafana-cloudalerting-and-irmalertingconfi… [truncated]
0:27
0:27
0:27
0:30
08 · How to Fix

How to Fix REPRO-2026-00227

Upgrade grafana/grafana · Go to Commit 8891796ca1086cd234e1715ea71d8db0073cc160 or later.

Coming soon

Step-by-step mitigation and hardening guidance for REPRO-2026-00227 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: REPRO-2026-00227

How does the Grafana service account enumeration exploit work?

A low-privileged user with a scoped serviceaccounts:read grant for only one service account (e.g. alpha-sa) calls GET /apis/iam.grafana.app/v0alpha1/namespaces/default/serviceaccounts on a vulnerable build and receives both alpha-sa and an unauthorized service account (beta-sa) in the response; the fixed build filters the response to only alpha-sa.

Which Grafana versions are affected, and where is it fixed?

Builds prior to commit 8891796ca1086cd234e1715ea71d8db0073cc160 are affected — that commit adds the missing "serviceaccounts" entry to the iam.grafana.app RBAC allowlist. The reproduction anchors the vulnerable build to that commit's parent, c00083433312adb7b7cfef83f74751e1216f67f8.

How severe is this Grafana issue?

It is rated high severity: an authorization bypass / information disclosure that lets low-privileged users enumerate service accounts beyond their granted scope.

How can I reproduce this Grafana RBAC bypass?

Download the verified script from this page and run it in an isolated environment against a Grafana unified-storage build at commit c00083433312adb7b7cfef83f74751e1216f67f8 (the vulnerable parent commit). It grants a user scoped access to a single service account, lists service accounts via the IAM API, and shows unauthorized accounts leaking in the response; the patched commit filters them out.
11 · References

References for REPRO-2026-00227

Authoritative sources for REPRO-2026-00227 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.