A leaky faucet in Grafana's avatar plumbing that eventually floods the basement
CVE-2026-21720 is a goroutine leak in Grafana's /avatar/:hash endpoint. When a user avatar isn't cached, Grafana spawns a background goroutine to fetch it from Gravatar. That goroutine sits in a 10-slot worker queue; if it waits longer than three seconds, the HTTP handler times out and stops listening — but the goroutine keeps running and tries to send its result on an unbuffered channel with no receiver, so it blocks forever. Every request with a fresh random hash leaks one goroutine and its associated memory. Sustained traffic exhausts memory and crashes the process. Affected: Grafana 3.0.0–11.6.8, 12.0.0–12.0.7, 12.1.0–12.1.4, 12.2.0–12.2.2, and 12.3.0.
Vendor severity is HIGH / 7.5 because the CVSS vector (AV:N/AC:L/PR:N/UI:N/A:H) rewards unauthenticated network reachability and full availability impact. That math is technically correct but ignores the real world: Grafana is overwhelmingly an internal service (SSO'd dashboards for eng/SRE), the impact is *just* DoS (no confidentiality or integrity loss), and the exploit is a slow-burn memory pressure attack, not an instant crash. HIGH overstates the operational reality for the ~95% of installs behind a VPN or reverse proxy.
4 steps from start to impact.
Reach the /avatar/:hash endpoint
/avatar/:hash route is served before authentication on most builds so login pages can render user avatars, meaning no credentials are needed.- Network path to Grafana HTTP(S) port (typically 3000)
- Endpoint not blocked by upstream WAF/reverse proxy
- Grafana is rarely internet-exposed — most installs sit behind SSO, corporate VPN, or an ingress with auth
- Shodan shows a small fraction of Grafana instances directly on the public internet
/api/healthSend a flood of unique avatar hashes
GET /avatar/<random-md5> at high rate. Each unique hash bypasses Grafana's avatar cache and forces a new goroutine to be enqueued. Trivially scriptable with curl, hey, or wrk — no exploit code required.- Sufficient request throughput to fill the 10-slot queue faster than Gravatar responds
- Rate-limiting reverse proxies (nginx
limit_req, Cloudflare, Traefik middleware) neutralize this immediately - Egress firewalls blocking outbound to
gravatar.comcause faster fetch failures, actually accelerating the leak — a wrinkle
/avatar/* shows up in access logs and any RUM/APM toolTrigger the goroutine leak
- Grafana's default worker queue depth of 10
- Gravatar latency or upstream failure > 3s
- Not applicable — this is the vulnerable code path itself
go_goroutines metric on Grafana's own /metrics climbs linearly; RSS grows without boundExhaust process memory → crash / OOMKill
- No memory ceiling / auto-restart that outpaces the leak
- No horizontal replication to absorb the outage
- HA Grafana behind a load balancer just means all replicas leak in parallel — not real friction, more like a slower fuse
- Kubernetes liveness restarts contain the blast but don't stop the DoS
kube_pod_container_status_restarts_total, incident channels lighting upThe supporting signals.
| CVE | CVE-2026-21720 |
|---|---|
| CWE | CWE-833 (Deadlock) / CWE-400 (Uncontrolled Resource Consumption) |
| In-the-wild exploitation | None observed. Not on CISA KEV. No reports on GreyNoise tagged to this CVE. |
| Public PoC | None weaponized. The bug is trivially reproducible with a shell one-liner (for i in $(seq 1 100000); do curl -s http://target/avatar/$(openssl rand -hex 16) & done) — no exploit kit needed. |
| EPSS | Currently <0.1% (near-floor) — consistent with a DoS bug in an internal-facing dashboard tool |
| KEV status | Not listed |
| CVSS v3.0 vector | AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H → 7.5 HIGH. Note the C:N/I:N — availability only. |
| Affected versions | 3.0.0 – 11.6.8, 12.0.0 – 12.0.7, 12.1.0 – 12.1.4, 12.2.0 – 12.2.2, 12.3.0 |
| Fixed versions | 11.6.9+security-01, 12.0.8+security-01, 12.1.5+security-01, 12.2.3+security-01, 12.3.1+security-01 |
| Exposure telemetry | Shodan product:Grafana returns roughly ~110K hosts globally; most are behind auth pages. Direct /avatar/* reachability without auth is common but the population that matters (public internet) is small. |
| Disclosed | 2026-01-27 by Grafana Labs Security |
| Reporter | Grafana Labs internal security team |
noisgate verdict.
The single decisive factor is impact ceiling: availability-only DoS on a monitoring service, with no confidentiality or integrity loss and no lateral movement primitive. Grafana is not a role-multiplier component (it does not hold identity, secrets, hypervisor, or data-tier authority), so even a successful crash is bounded to dashboard downtime on one service.
Why this verdict
- Impact ceiling is DoS. The CVSS vector is
C:N/I:N/A:H— attacker gets a crashed dashboard, not a shell, not creds, not data. That alone caps operational severity below HIGH for most environments. - Exposure narrows further. Grafana is predominantly an internal / VPN-gated service. The subset of instances directly reachable on the internet with the pre-auth
/avatar/*path exposed is the actual target population — a minority of installs. - Role multiplier: low. Grafana is a visualization tier — (a) low-value role: dev/test dashboards, (b) typical role: production observability UI, (c) high-value role: on-call alert rendering / unified alerting. Even in the high-value role, crash impact is *degraded visibility*, not identity or data compromise. No canonical Grafana deployment ends in fleet compromise from this bug. Floor is MEDIUM.
- Trivial mitigation. A one-line nginx
limit_reqor a WAF rule on/avatar/*kills the exploit outright — no patching pressure equivalent to a memory-corruption bug. - No exploitation signal. Not on KEV, EPSS near-floor, no public exploit tooling. Attackers optimize for RCE/creds, not DoSing someone's Grafana.
Why not higher?
HIGH is reserved for bugs where the blast radius plausibly reaches a domain, fleet, tenant, or supply chain. A goroutine leak that crashes a monitoring UI does not. The vendor's 7.5 is a mechanical CVSS calculation that treats "process crash" as high impact regardless of the process's role.
Why not lower?
Not LOW because unauthenticated attackers can trigger it with zero skill, and losing observability during an active incident has real operational cost (alert blindness). Not IGNORE because internet-exposed Grafana instances (a minority but non-trivial population) are genuinely at risk of hostile DoS during opportunistic scanning.
What to do — in priority order.
- Rate-limit
/avatar/*at the reverse proxy — Add nginxlimit_req_zone/ TraefikRateLimitmiddleware / Cloudflare rule capping/avatar/*to a few requests per second per IP. This starves the goroutine leak before it can accumulate. No mitigation SLA for MEDIUM under noisgate — apply within the 365-day remediation window, but if the Grafana instance is internet-facing, treat this as a same-week task. - Verify Grafana is not internet-exposed — Confirm all Grafana ingress points require SSO / VPN / IP allowlist before hitting the app. Anonymous access to
/avatar/*is what makes this exploitable pre-auth. Audit reverse proxy configs and cloud load balancer listeners this month. - Set memory limits + auto-restart — In Kubernetes, ensure Grafana pods have
resources.limits.memoryset and a liveness probe that restarts on hang. On VMs, run Grafana under systemd withMemoryMax=andRestart=always. Turns a full outage into a brief blip while you patch. - Alert on
go_goroutinesgrowth — Grafana exposes/metrics. A Prometheus rule alerting whengo_goroutines{job="grafana"}grows monotonically over 30 min catches this bug being exploited AND catches other future goroutine leaks. - Patch to a
+security-01release on the 365-day remediation window — Track upgrade to11.6.9+security-01,12.0.8+security-01,12.1.5+security-01,12.2.3+security-01, or12.3.1+security-01. Because 12.0.x is EOL, plan a version-line migration to 12.3.x or the current LTS at the same time — noisgate remediation SLA for MEDIUM is ≤ 365 days.
- Authentication won't save you —
/avatar/:hashis deliberately served before login so avatar images render on the login page; requiring auth on the dashboard doesn't gate this endpoint. - Egress blocking of gravatar.com makes it WORSE — a firewalled egress to Gravatar means the fetch fails slowly, which is exactly the condition (>3s wait) that triggers the leak.
- Restarting Grafana on a cron buys hours but is not mitigation; a modest attacker can leak enough memory between restarts to cause repeated outages during business hours.
- A WAF signature for 'Grafana exploit' — there is nothing anomalous in the request payload. Only volumetric rate-limiting on the path works.
Crowdsourced verification payload.
Run on the Grafana host (or any host with SSH access to it) as an unprivileged user. Invoke as ./check_grafana_cve_2026_21720.sh http://localhost:3000 — no root needed. Script queries the Grafana health endpoint for its version and compares against the fixed set.
#!/usr/bin/env bash
# check_grafana_cve_2026_21720.sh
# Detects CVE-2026-21720 (Grafana avatar goroutine-leak DoS)
# Usage: ./check_grafana_cve_2026_21720.sh <grafana_base_url>
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -u
BASE="${1:-http://localhost:3000}"
JSON=$(curl -fsS --max-time 5 "${BASE%/}/api/health" 2>/dev/null) || {
echo "UNKNOWN: could not reach ${BASE}/api/health"
exit 2
}
VER=$(echo "$JSON" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
if [ -z "$VER" ]; then
echo "UNKNOWN: could not parse version from /api/health"
exit 2
fi
echo "Detected Grafana version: $VER"
# Strip +security-01 suffix marker before numeric compare
CORE=$(echo "$VER" | cut -d+ -f1)
SUFFIX=$(echo "$VER" | grep -o '+security-[0-9]*' || true)
MAJ=$(echo "$CORE" | cut -d. -f1)
MIN=$(echo "$CORE" | cut -d. -f2)
PAT=$(echo "$CORE" | cut -d. -f3)
vuln() { echo "VULNERABLE: $VER is affected by CVE-2026-21720"; exit 1; }
safe() { echo "PATCHED: $VER contains the CVE-2026-21720 fix"; exit 0; }
# Fixed floors per line:
# 11.6.9+security-01, 12.0.8+security-01, 12.1.5+security-01,
# 12.2.3+security-01, 12.3.1+security-01, >= 12.4.0
case "$MAJ.$MIN" in
11.6)
if [ "$PAT" -gt 9 ] || { [ "$PAT" -eq 9 ] && [ -n "$SUFFIX" ]; }; then safe; else vuln; fi ;;
12.0)
if [ "$PAT" -gt 8 ] || { [ "$PAT" -eq 8 ] && [ -n "$SUFFIX" ]; }; then safe; else vuln; fi ;;
12.1)
if [ "$PAT" -gt 5 ] || { [ "$PAT" -eq 5 ] && [ -n "$SUFFIX" ]; }; then safe; else vuln; fi ;;
12.2)
if [ "$PAT" -gt 3 ] || { [ "$PAT" -eq 3 ] && [ -n "$SUFFIX" ]; }; then safe; else vuln; fi ;;
12.3)
if [ "$PAT" -gt 1 ] || { [ "$PAT" -eq 1 ] && [ -n "$SUFFIX" ]; }; then safe; else vuln; fi ;;
*)
if [ "$MAJ" -gt 12 ] || { [ "$MAJ" -eq 12 ] && [ "$MIN" -ge 4 ]; }; then safe; fi
if [ "$MAJ" -lt 11 ] || { [ "$MAJ" -eq 11 ] && [ "$MIN" -lt 6 ]; }; then vuln; fi
echo "UNKNOWN: version $VER is outside the known matrix — verify manually"
exit 2
;;
esac
If you remember one thing.
/avatar/* today — this neutralizes the exploit regardless of Grafana version. (3) Because noisgate reassesses this as MEDIUM, there is no mitigation SLA — go straight to the 365-day remediation window for internal-only instances, but shorten it to ≤ 30 days for anything reachable from the internet. (4) The noisgate remediation SLA target is ≤ 365 days: fold the upgrade to 11.6.9+security-01 or the appropriate +security-01 build (or 12.4.x if migrating off the EOL 12.0 line) into your normal Grafana maintenance train. Do not lose sleep over this one — it is a memory leak that crashes a dashboard, not a foothold.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.