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.
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.
REPRO-2026-00227 Severity
REPRO-2026-00227 is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
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 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 Proof of Reproduction for REPRO-2026-00227
- 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
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
- Real Grafana unified-storage HTTP endpoint GET /apis/iam.grafana.app/v0alpha1/namespaces/{org}/serviceaccounts
- IAM serviceaccount List
- common.List
- 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
Root Cause and Exploit Chain for REPRO-2026-00227
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
8891796ca1086cd234e1715ea71d8db0073cc160where theiam.grafana.appallowlist 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_bypassover a remote/API surface: service account enumeration throughGET /apis/iam.grafana.app/.../namespaces/{org}/serviceaccountsby a low-privileged authenticated user without permission for the enumerated service account(s). - Reproduced impact from this run:
authz_bypassthrough the real Grafana HTTP/API path. The script starts a real Grafana test server with unified storage and Mode5 forserviceaccounts.iam.grafana.app, createsalpha-saandbeta-sa, creates a low-privileged Viewer user with onlyserviceaccounts:readscoped toalpha-sa, then performs a raw HTTPGET /apis/iam.grafana.app/v0alpha1/namespaces/default/serviceaccountsas that user. Vulnerable build returnsalpha-saand unauthorizedbeta-sa; fixed build returns onlyalpha-sa. - Parity:
fullfor the product-facing API authorization-bypass class. The caller is low-privileged and lacks permission to readbeta-sa, yet receives it only on the vulnerable build. - Not demonstrated: The repository currently serves IAM APIs as
v0alpha1only (apps/iam/pkg/apis/iam_manifest.gohasPreferredVersion: "v0alpha1"and nov1service 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:
Checkreturnsclaims.CheckResponse{Allowed: true}.Compilereturnsfunc(name, folder string) bool { return true }, so list filtering allows every object.BatchCheckmarks 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
- Run
bundle/repro/reproduction_steps.sh. - The script:
- Reads
bundle/project_cache_context.jsonand reuses the stable Grafana checkout at<project_cache_dir>/repo. - Uses the fixed commit
8891796ca1086cd234e1715ea71d8db0073cc160and its parentc00083433312adb7b7cfef83f74751e1216f67f8. - Verifies the vulnerable checkout lacks the
serviceaccountsallowlist entry and the fixed checkout contains it. - Runs a supporting real-code library test against
authzLimitedClienton 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.
- Reads
- Expected evidence:
- Vulnerable HTTP run:
HTTP_LIST_RESULT_JSON {"items":2,"names":["alpha-sa","beta-sa"],..."status":200}andBEHAVIOUR: VULNERABLE_HTTP. - Fixed HTTP run:
HTTP_LIST_RESULT_JSON {"items":1,"names":["alpha-sa"],..."status":200}andBEHAVIOUR: FIXED_HTTP. - Script exits
0only when both the vulnerable product exposure and fixed negative control are observed.
- Vulnerable HTTP run:
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.logandbundle/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
8891796ca1086cd234e1715ea71d8db0073cc160soserviceaccountsis included in theiam.grafana.appRBAC allowlist. - Add regression tests for the product HTTP list path: a low-privileged user with
serviceaccounts:readscoped 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.goat 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
authzLimitedClienttest 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; nov1IAM service account API is served inapps/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.
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]Artifacts and Evidence for REPRO-2026-00227
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix REPRO-2026-00227
Upgrade grafana/grafana · Go to Commit 8891796ca1086cd234e1715ea71d8db0073cc160 or later.
FAQ: REPRO-2026-00227
How does the Grafana service account enumeration exploit work?
Which Grafana versions are affected, and where is it fixed?
How severe is this Grafana issue?
How can I reproduce this Grafana RBAC bypass?
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.