Authorization bypass / Access control due to UID truncation
Description
The commit fixes an input validation/authorization edge-case by increasing the maximum length for role identifiers (role UID and role name) and expanding the storage column for role.uid from 40 to 253 characters. Previously, long role UIDs could be truncated by the storage layer (e.g., MySQL) or by API boundaries, potentially causing mismatches in access-control decisions and allowing misauthorization. The changes unify and enforce a clear maximum length (253) to prevent silent truncation and ensure proper validation before authorization checks.
Proof of Concept
Proof of Concept (illustrative; endpoints and exact behavior depend on Grafana deployment):
Prerequisites:
- Grafana instance using SQL storage (MySQL) with legacy role.uid length of 40 before the patch.
- Administrative access to create roles and manage permissions.
- API client (curl, Postman, or a small script).
Idea:
- Demonstrate that, prior to this fix, a long role UID could be truncated by storage boundaries (e.g., MySQL varchar(40)) and that this truncation could lead to misauthorization when the truncated UID collided with another role’s UID or when identity binding is affected by the truncated value.
Steps (before fix scenario):
1) Create two roles with colliding/truncated UIDs after storage truncation
- Role A UID long_uid_A = 'A' repeated 300 times
- Role B UID long_uid_B = 'A' repeated 40 times + 'B' repeated 260 times (so long_uid_B starts with the same 40 'A's as long_uid_A)
- Give Role B elevated permissions (e.g., access to a sensitive dashboard)
2) Create roles via Grafana API (endpoints are deployment-specific; placeholders shown):
- POST https://grafana.example.com/api/iam/roles
Headers: Authorization: Bearer <admin_token>
Body: {"uid": "<long_uid_A>", "name": "RoleA_Long", "permissions": ["read:restricted"]}
- POST https://grafana.example.com/api/iam/roles
Headers: Authorization: Bearer <admin_token>
Body: {"uid": "<long_uid_B>", "name": "RoleB", "permissions": ["read:restricted", "write:config"]}
3) Attempt to operate as Role A (e.g., request a restricted resource) that would be authorized by Role B if the UID were truncated to the legacy length (e.g., 40 chars). The system might map long_uid_A to the same truncated value as long_uid_B and grant Role B’s permissions.
4) Expected pre-fix behavior (illustrative):
- The storage truncates long_uid_A to 40 chars and the authorization decision incorrectly uses the truncated value, resulting in misauthorization (Role B permissions granted) or similar misbinding.
5) Validate post-fix behavior (as implemented by this commit):
- The API rejects UIDs longer than 253 characters with a validation error, preventing silent truncation.
- UIDs up to 253 chars are accepted and stored consistently, ensuring that authorization is based on the exact UID value, not a truncated form.
Alternate lightweight test (DB level, pre-fix):
- If your deployment still uses a 40-char uid column, you can reproduce truncation at the DB level:
CREATE TABLE role (uid VARCHAR(40) NOT NULL, PRIMARY KEY (uid));
INSERT INTO role (uid) VALUES (REPEAT('A', 100));
SELECT LENGTH(uid) AS len, uid FROM role; -- len will be 40
Note: The exact Grafana API endpoints and role/UID semantics can vary by version and deployment. The PoC above is a conceptual demonstration of how long UIDs could be truncated and lead to misauthorization prior to the fix. A complete PoC in a live environment would require aligning with the specific enterprise API surface and authorization checks used by the Grafana deployment.
Python example (conceptual only, endpoints may differ):
import requests
BASE = "https://grafana.example.com"
ADMIN_TOKEN = "<admin_token>"
HEADERS = {"Authorization": f"Bearer {ADMIN_TOKEN}", "Content-Type": "application/json"}
# Create Role A with a very long UID
long_uid_A = "A" * 300
resp = requests.post(f"{BASE}/api/iam/roles", headers=HEADERS, json={"uid": long_uid_A, "name": "RoleA_Long", "permissions": ["read:restricted"]})
print(resp.status_code, resp.text)
# Create Role B with a UID that would collide under truncation (legacy length 40)
long_uid_B = "A" * 40 + "B" * 260
resp = requests.post(f"{BASE}/api/iam/roles", headers=HEADERS, json={"uid": long_uid_B, "name": "RoleB", "permissions": ["read:restricted", "write:config"]})
print(resp.status_code, resp.text)
# Attempt an operation that would be governed by Role B but could be affected by truncation if the long UID is stored inaccurately
# The exact API for performing a protected action depends on deployment; this step is illustrative.
Notes:
- Replace endpoints with the actual Grafana Enterprise API surface for roles and permissions in your environment.
- The key proof is that before the fix, long UIDs could be truncated by storage, enabling misauthorization; after the fix, such truncation is prevented by validation and storage length alignment.
Commit Details
Author: Alexander Zobnin
Date: 2026-05-29 08:21 UTC
Message:
Access Control: Fix role UID length validation (#123332)
* Access Control: Fix role UID length validation
* remove unused code
* limit role max length to 253
Triage Assessment
Vulnerability Type: Authorization bypass / Access control
Confidence: HIGH
Reasoning:
The commit tightens and unifies UID/Name length validation to a fixed maximum (253) to prevent silent truncation at storage or API boundaries. This prevents potential misauthorization due to truncated role identifiers and ensures proper validation before access control decisions, addressing an input validation/authentication edge case that could be exploited to bypass or alter access controls.
Verification Assessment
Vulnerability Type: Authorization bypass / Access control due to UID truncation
Confidence: HIGH
Affected Versions: 12.0.0 - 12.4.0 (pre-fix)
Code Diff
diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go
index b2ce8c2aca0e7..a18be72f4af7b 100644
--- a/pkg/registry/apis/iam/register.go
+++ b/pkg/registry/apis/iam/register.go
@@ -399,6 +399,18 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge
MaximumNameLength: 80,
RequireDeprecatedInternalID: true,
})
+ // Cap the apiserver name at 253 characters so callers get a clear
+ // validation error instead of a silent truncation/error at the storage
+ // layer. 253 is the Kubernetes DNS-1123 subdomain limit for metadata.name
+ // and also matches the size of the `name` column in the unified storage
+ // `resource`/`resource_history` tables, as well as the `role.uid` column
+ // expansion done by the legacy migration in this PR.
+ opts.StorageOptsRegister(iamv0.RoleInfo.GroupResource(), apistore.StorageOptions{
+ MaximumNameLength: 253,
+ })
+ opts.StorageOptsRegister(iamv0.GlobalRoleInfo.GroupResource(), apistore.StorageOptions{
+ MaximumNameLength: 253,
+ })
if enableTeamsApi {
if err := b.UpdateTeamsAPIGroup(opts, storage, enableZanzanaSync); err != nil {
diff --git a/pkg/services/sqlstore/migrations/accesscontrol/migrations.go b/pkg/services/sqlstore/migrations/accesscontrol/migrations.go
index a0e1704b8ad87..a3c44ad7694ef 100644
--- a/pkg/services/sqlstore/migrations/accesscontrol/migrations.go
+++ b/pkg/services/sqlstore/migrations/accesscontrol/migrations.go
@@ -231,5 +231,15 @@ func AddMigration(mg *migrator.Migrator) {
Name: "datasource_type", Type: migrator.DB_NVarchar, Length: 255, Nullable: true,
}))
+ // Expand role.uid column from 40 to 253 so that longer UIDs are no longer
+ // silently truncated by MySQL. 253 matches both the Kubernetes DNS-1123
+ // subdomain limit for metadata.name and the size of the `name` column in
+ // the unified storage `resource`/`resource_history` tables, keeping a
+ // single ceiling across the legacy and apiserver paths. SQLite does not
+ // enforce VARCHAR length so the change is a no-op there.
+ mg.AddMigration("Expand role.uid length to 253", migrator.NewRawSQLMigration("").
+ Postgres("ALTER TABLE role ALTER COLUMN uid TYPE VARCHAR(253);").
+ Mysql("ALTER TABLE role MODIFY uid NVARCHAR(253) NOT NULL;"))
+
AddDatasourceTypeMigration(mg)
}