{"repro_id":"REPRO-2026-00226","version":6,"title":"Grafana IAM LIST authorization could be bypassed for folder-scoped CRDs when a user had wildcard resource permissions, returning `All: true` without folder-level checks.","repro_type":"security","status":"published","severity":"high","description":"Grafana's listPermission for mapper-miss resources (folder-scoped CRDs like *.ext.grafana.app) checked scopeMap[\"*\"] and returned All: true before invoking folder-scoped authorization. A user with a resource-type wildcard permission (e.g., widget.ext.grafana.app/widgets:get with scope *) could list all objects across folders without folder-level authorization.","root_cause":"## Summary\n\nGrafana IAM LIST authorization for folder-scoped Kubernetes-native/custom resources could be bypassed when the requesting identity had a resource-type wildcard permission (`scope: \"*\"`) for the CRD action but lacked folder-level authorization. In the vulnerable parent of fix commit `b9b897b3c512ee434341bb9d698eac24f90eca89`, `pkg/services/authz/rbac.Service.listPermission` returned `ListResponse{All: true}` as soon as `scopeMap[\"*\"]` was present, before detecting mapper-miss resources such as `widget.ext.grafana.app/widgets` and before applying the folder-scoped authorization model. The current reproduction starts a real TCP gRPC `authz.v1.AuthzService` endpoint, sends a LIST request over `/authz.v1.AuthzService/List`, and shows that the vulnerable commit returns `All=true` while the fixed commit returns `All=false` for the same request and permission state.\n\n## Impact\n\n- **Package/component affected:** `github.com/grafana/grafana`, specifically `pkg/services/authz/rbac` and the Grafana IAM/AuthZ gRPC LIST authorization path.\n- **Affected versions:** Builds containing the pre-fix behavior before `b9b897b3c512ee434341bb9d698eac24f90eca89` (`IAM: folder-scoped authz with LIST (#126931)`). The reproduction anchors the vulnerable checkout to the fixed commit's parent: `27750f0e0e3443c39f992bfe22efe3d352ee4357`.\n- **Risk level and consequences:** High. A user or service identity with only a wildcard resource permission for a folder-scoped CRD action (for example `widget.ext.grafana.app/widgets:get` with `scope: \"*\"`) could receive `All: true` from the AuthZ LIST API. That response authorizes listing across all folders even when the identity has no folder-level access, bypassing folder-scoped authorization boundaries.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Authorization bypass for folder-scoped CRD LIST authorization through the Grafana IAM/AuthZ API surface.\n- **Reproduced impact from this run:** Authorization bypass was reproduced over the real gRPC AuthzService LIST API boundary. The vulnerable commit returned `ListResponse All=true` for a folder-scoped mapper-miss CRD when the subject had only wildcard resource permission and no folder authorization. The fixed commit returned `All=false` for the same request.\n- **Parity:** `full` for the claimed authorization-bypass behavior and API surface.\n- **Not demonstrated:** The proof does not enumerate actual end-user CRD objects in a full Grafana deployment. It demonstrates the authorization decision primitive (`All=true`) that the LIST caller would use to list all objects.\n\n## Root Cause\n\nThe root cause is an ordering error in `pkg/services/authz/rbac/service.go` inside `Service.listPermission`. Before the fix, `listPermission` checked `scopeMap[\"*\"]` before distinguishing mapper-hit and mapper-miss resources. Grafana's scope-map construction collapses wildcard grants into `scopeMap[\"*\"]`; for ordinary resources that can be an all-resource allow. However, folder-scoped Kubernetes-native CRDs that miss the static mapper (for example groups ending in `.ext.grafana.app`) require a stricter model: the caller must satisfy both stack/resource permission and folder-level authorization.\n\nBecause the vulnerable implementation returned `ListResponse{All: true}` immediately when `scopeMap[\"*\"]` was present, folder-scoped CRDs skipped the `listPermissionWithFolderAuthz` logic entirely. The fix in `b9b897b3c512ee434341bb9d698eac24f90eca89` moves the mapper-miss/folder-scoped branch before the wildcard early return:\n\n- Vulnerable parent `27750f0e0e3443c39f992bfe22efe3d352ee4357`: `scopeMap[\"*\"]` can return `All=true` before folder authorization.\n- Fixed commit `b9b897b3c512ee434341bb9d698eac24f90eca89`: mapper-miss resources first call `listPermissionWithFolderAuthz`, so wildcard resource permission alone does not yield `All=true`.\n\nFix commit: `https://github.com/grafana/grafana/commit/b9b897b3c512ee434341bb9d698eac24f90eca89`\n\n## Reproduction Steps\n\n1. Use `bundle/repro/reproduction_steps.sh`.\n2. The script reads `bundle/project_cache_context.json`, reuses the prepared Grafana checkout at `<project_cache_dir>/repo`, resolves the fixed commit and its parent, and verifies that the vulnerable parent lacks the `listPermissionWithFolderAuthz` fork while the fixed commit contains it.\n3. The script writes and injects `bundle/repro/repro_grpc_boundary_test.go` into `pkg/services/authz/rbac` for each checkout. That test starts a real TCP gRPC server, registers Grafana's real `rbac.Service` via `authzv1.RegisterAuthzServiceServer`, performs a TCP health check, and sends an attacker-controlled `ListRequest` over `/authz.v1.AuthzService/List`.\n4. The request uses `namespace=org-12`, `subject=user:test-uid`, `group=widget.ext.grafana.app`, `resource=widgets`, `verb=list`. The configured identity has only `widget.ext.grafana.app/widgets:get` with `scope=\"*\"` and no folder-level permission.\n5. The script runs two vulnerable attempts and two fixed attempts. Expected evidence is vulnerable `All=true` in both attempts and fixed `All=false` in both attempts.\n\n## Evidence\n\nPrimary evidence files:\n\n- `bundle/logs/reproduction_steps.log` — current-run summary and request/response excerpts for all four attempts.\n- `bundle/logs/vuln_grpc_attempt1.log` and `bundle/logs/vuln_grpc_attempt2.log` — vulnerable API-boundary request/response logs.\n- `bundle/logs/fixed_grpc_attempt1.log` and `bundle/logs/fixed_grpc_attempt2.log` — fixed negative-control API-boundary request/response logs.\n- `bundle/repro/runtime_manifest.json` — runtime evidence manifest showing `entrypoint_kind=\"api_remote\"`, `service_started=true`, `healthcheck_passed=true`, and `target_path_reached=true`.\n- `bundle/repro/repro_grpc_boundary_test.go` — the gRPC boundary test code used by the script.\n\nKey excerpts from `bundle/logs/reproduction_steps.log`:\n\n```text\nPatch check: vulnerable parent lacks listPermissionWithFolderAuthz fork; fixed commit contains it\n\nVULNERABLE attempt 1 request/response evidence:\nSERVER: Grafana AuthzService gRPC endpoint listening on 127.0.0.1:39885\nHEALTHCHECK: TCP connection to Grafana AuthzService endpoint 127.0.0.1:39885 succeeded\nCLIENT: sending LIST over gRPC /authz.v1.AuthzService/List namespace=org-12 subject=user:test-uid group=widget.ext.grafana.app resource=widgets verb=list permission=widget.ext.grafana.app/widgets:get scope=* no_folder_permission=true\nSERVER: accepted gRPC method=/authz.v1.AuthzService/List namespace=org-12 subject=user:test-uid group=widget.ext.grafana.app resource=widgets verb=list token_prefix=pruva\nSERVER: completed gRPC method=/authz.v1.AuthzService/List response All=true Folders=[] Items=[] err=<nil>\nCLIENT: received ListResponse: All=true Folders=[] Items=[]\n\nFIXED attempt 1 request/response evidence:\nSERVER: Grafana AuthzService gRPC endpoint listening on 127.0.0.1:43707\nHEALTHCHECK: TCP connection to Grafana AuthzService endpoint 127.0.0.1:43707 succeeded\nCLIENT: sending LIST over gRPC /authz.v1.AuthzService/List namespace=org-12 subject=user:test-uid group=widget.ext.grafana.app resource=widgets verb=list permission=widget.ext.grafana.app/widgets:get scope=* no_folder_permission=true\nSERVER: accepted gRPC method=/authz.v1.AuthzService/List namespace=org-12 subject=user:test-uid group=widget.ext.grafana.app resource=widgets verb=list token_prefix=pruva\nSERVER: completed gRPC method=/authz.v1.AuthzService/List response All=false Folders=[] Items=[] err=<nil>\nCLIENT: received ListResponse: All=false Folders=[] Items=[]\n\nSummary:\nVulnerable attempt 1: All=true\nVulnerable attempt 2: All=true\nFixed attempt 1: All=false\nFixed attempt 2: All=false\nCONFIRMED: vulnerable Grafana AuthzService gRPC LIST returns All=true across the remote API boundary, while the fixed commit returns All=false.\n```\n\nEnvironment details captured:\n\n- Go toolchain: `go version go1.26.4 linux/amd64`.\n- Repository path: `/data/pruva/project-cache/6c6f6fd2-6e61-4267-8db1-032ee6a303f9/repo` from the prepared project cache.\n- Vulnerable commit: `27750f0e0e3443c39f992bfe22efe3d352ee4357`.\n- Fixed commit: `b9b897b3c512ee434341bb9d698eac24f90eca89`.\n\n## Recommendations / Next Steps\n\n- Keep the fix's ordering: route mapper-miss/folder-scoped resources to `listPermissionWithFolderAuthz` before honoring `scopeMap[\"*\"]`.\n- Add regression coverage that exercises the public gRPC `AuthzService/List` boundary for folder-scoped CRDs with wildcard resource permission but no folder permission.\n- Audit other authorization paths for wildcard early returns before resource-specific or folder-specific constraints are applied.\n- Upgrade Grafana deployments to a version containing `b9b897b3c512ee434341bb9d698eac24f90eca89` or an equivalent backport.\n\n## Additional Notes\n\n- The reproduction script was run twice consecutively in this workspace and succeeded both times.\n- Each script run performs two vulnerable attempts and two fixed attempts, so the final current-run evidence contains four gRPC request/response executions.\n- The proof uses the real Grafana AuthZ gRPC service handler and protobuf client/server boundary. Test scaffolding supplies deterministic identity/permission/folder data so the vulnerable authorization branch is reached repeatably without requiring an external Grafana deployment.","cwe_id":"CWE-863","source_url":"https://github.com/spaceraccoon/vulnerability-spoiler-alert/issues/292","package":{"name":"grafana/grafana","ecosystem":"Go","affected_versions":"Versions prior to commit b9b897b3c512ee434341bb9d698eac24f90eca89 (folder-scoped LIST authz check occurs after wildcard scope check)","fixed_version":"b9b897b3c512ee434341bb9d698eac24f90eca89"},"reproduced_at":"2026-07-04T19:53:31.434536+00:00","duration_secs":1321.0,"tool_calls":277,"handoffs":3,"total_cost_usd":10.28826157,"agent_costs":{"judge":0.0240627,"repro":10.18631884,"support":0.07788003},"cost_breakdown":{"judge":{"gpt-5.4-mini":0.0240627},"repro":{"accounts/fireworks/routers/glm-5p2-fast":1.3590518399999998,"gpt-5.5":8.827267},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.07788003}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-04T19:53:49.077028+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":17767,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":9290,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":5523,"category":"other"},{"path":"bundle/repro/repro_bypass_test.go","filename":"repro_bypass_test.go","size":3888,"category":"other"},{"path":"bundle/logs/vuln_test.log","filename":"vuln_test.log","size":531,"category":"log"},{"path":"bundle/logs/fixed_test.log","filename":"fixed_test.log","size":479,"category":"log"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":781,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":968,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":6845,"category":"log"},{"path":"bundle/logs/vuln_grpc_attempt1.log","filename":"vuln_grpc_attempt1.log","size":1275,"category":"log"},{"path":"bundle/logs/fixed_grpc_attempt1.log","filename":"fixed_grpc_attempt1.log","size":1277,"category":"log"},{"path":"bundle/logs/vuln_grpc_attempt2.log","filename":"vuln_grpc_attempt2.log","size":1275,"category":"log"},{"path":"bundle/logs/fixed_grpc_attempt2.log","filename":"fixed_grpc_attempt2.log","size":1277,"category":"log"},{"path":"bundle/repro/repro_grpc_boundary_test.go","filename":"repro_grpc_boundary_test.go","size":6057,"category":"other"}]}