Re-exploring CVE-2021-43798 in Grafana

How to explore, detect, and block with NetworkPolicies and Admission Policies

CTO

João Brito

Pre-authentication Directory Traversal that reads local files — exploitation, detection, and containment policies

TL;DR

This is a re-exploration of CVE-2021-43798, which is a pre-auth path traversal vulnerability in the endpoint /public/plugins/<plugin-id>/... of Grafana 8.x. An attacker can read local files from the server/container (e.g., /etc/grafana/grafana.ini, /var/lib/grafana/grafana.db, /proc/self/environ) using valid plugin IDs — many come by default, making exploitation trivial. Versions 8.0.0-beta1 to 8.3.0 are affected; the fix was released in 8.0.7 / 8.1.8 / 8.2.7 / 8.3.1. Update immediately. If that’s not possible right now, mitigate with WAF/rewrites blocking ../, restrict exposure, and apply NetworkPolicies and Admission Policies to reduce the surface area and impact.


You might think that this CVE no longer makes sense, but there are still MANY outdated environments and at the same time, many targeted attacks against them, as shown by greynoise.


Phase 1 — What is the problem in this CVE

Technical summary of the bug

The static file handler for plugins in Grafana 8.x does not properly sanitize the requested path. By accessing GET /public/plugins/<plugin-id>/../../../../<file>, the server returns content outside the plugin directory, allowing local file read without authentication. The vector exists for any installed plugin (including built-ins).

Why is this relevant in 2025?
Despite being old, the vulnerability has remained widely exploitable for a long time due to improper exposure and late patching; analyses show repeated exploitation and probing even years after the fix. In environments where Grafana has credentials for data sources and critical integrations, reading local files becomes initial access when combined with other steps (e.g., decrypting secrets from grafana.db after obtaining the secret key).

Timeline and fixed builds
The fix was published on December 7, 2021 in versions 8.3.1 / 8.2.7 / 8.1.8 / 8.0.7 and detailed by Grafana Labs in subsequent posts.


Phase 2 — What it affects and exploitation scenarios

Affected versions and fix

  • Affected: 8.0.0-beta1 → 8.3.0;

  • Fixed: 8.0.7, 8.1.8, 8.2.7, 8.3.1.
    (Update to one of these series or higher.)

Prerequisites and attack surface

  • No authentication is required;

  • A valid plugin-id is sufficient (e.g., alertlist, annolist, barchart, bargauge, candlestick, cloudwatch, dashlist, elasticsearch — all commonly default).


Quick PoC (lab)

Attention: for educational purposes in a controlled environment.

# /etc/passwd via plugin built-in
curl -s http://GRAFANA:3000/public/plugins/alertlist/../../../../../../etc/passwd

# URL-encoded (costuma bypassar alguns filtros bobos)
curl -s "http://GRAFANA:3000/public/plugins/alertlist/..%2f..%2f..%2f..%2f..%2fetc%2fpasswd"

# Arquivos sensíveis dentro do container/VM do Grafana
curl -s http://GRAFANA:3000/public/plugins/alertlist/../../../../../../etc/grafana/grafana.ini
curl -s http://GRAFANA:3000/public/plugins/alertlist/../../../../../../var/lib/grafana/grafana.db
curl -s

References for public modules/PoCs (for understanding the vector): Exploit-DB and Metasploit module.


“And what after I read grafana.db?”

Grafana stores encrypted data sources credentials. The current model uses DEKs (data keys) stored in the database and protected by a KEK (Key Encryption Key) defined by secret_key (config or KMS). If an attacker reads the database (grafana.db) and the secret_key, there is a possibility of decrypting secrets depending on the configuration/version (e.g., when the secret_key locally protects the DEKs). In poorly configured setups (without KMS), this can expose passwords/tokens for data sources.

Practical scenarios in Kubernetes

  1. Grafana exposed (LoadBalancer/Ingress) without IP filtering and without WAF: reading grafana.ini + grafana.db ⇒ credential extraction and lateral movement to databases/observability;

  2. Grafana in-cluster but accessible from compromised Pods: local reading and exfiltration via open egress;

  3. Sensitive variables in environment: reading /proc/self/environ may reveal GF_SECURITY_ADMIN_PASSWORD, API keys, cloud tokens.


Phase 3 — How to fix (priorities and policies)

1) Update now

Roll out to a version 8.0.7 / 8.1.8 / 8.2.7 / 8.3.1 (or higher).


Recommendation: Update at least to the above versions and perform controlled rollout. Subscribe to the official Grafana image without CVEs at quor.dev.


2) Immediate hardening (if unable to update today)

Block path traversal at the edge (Nginx):

location ~* ^/public/plugins/.*/(\.\.|%2e%2e)/ {
  return 403;
}
# Proteção extra contra segmentos "dot-dot" mesmo sem barra
if ($request_uri ~* "/public/plugins/.*\.\.") { return

Apache (mod_rewrite):

RewriteEngine On
RewriteCond %{REQUEST_URI} ^/public/plugins/ [NC]
RewriteCond %{THE_REQUEST} \.\. [OR]
RewriteCond %{REQUEST_URI} \.\. [OR]
RewriteCond %{QUERY_STRING} \.\.
RewriteRule ^ - [F]

Grafana “behind” CDN/WAF: create a rule to deny ../ and %2f.. specifically under /public/plugins/.

3) NetworkPolicies (Kubernetes)

Reduce the ingress and egress surface of the Grafana Pod:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: grafana-minimal
  namespace: observability
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: grafana
  policyTypes: [Ingress, Egress]
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          name: platform
    ports:
    - protocol: TCP
      port: 3000
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          name: monitoring
    ports:
    - protocol: TCP
      port: 9090   # ex.: Prometheus
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
    ports:
    - protocol: TCP
      port: 443    # somente o estritamente necessário

Note: the CVE reads local files; NetworkPolicy does not prevent reading, but reduces exfiltration.

4) Admission Policies (CEL) for workload hygiene

Block privileged Pods and require digest for images (reducing the risk of vulnerable rollbacks and lateral movement after initial access via Grafana):

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: baseline-security
spec:
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE","UPDATE"]
        resources: ["pods"]
  validations:
    - expression: "object.spec.containers.all(c, c.image.matches('.*@sha256:.*'))"
      message: "Imagem deve estar fixada por digest"
    - expression: "has(object.spec.securityContext) && object.spec.securityContext.runAsNonRoot == true"
      message: "Pods devem rodar como não-root"
    - expression: |
        object.spec.containers.all(c,
          has(c.securityContext) &&
          c.securityContext.privileged != true &&
          c.securityContext.allowPrivilegeEscalation != true)
      message: "Sem privileged e sem allowPrivilegeEscalation"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: baseline-security-binding
spec:
  policyName: baseline-security
  validationActions: ["Deny"

5) Grafana secrets and encryption

  • Rotate secret_key and re-protect the DEKs;

  • Consider KMS (e.g., Azure Key Vault) for the KEK, removing static secret in the filesystem;

  • Minimize the value of grafana.db at rest (credentials through service accounts with minimal privileges, scopes and short lifespan).

Detection and response

Indicators in logs (Nginx/Ingress/LB)

  • Patterns in URI/args: ../, %2e%2e, %2f.. under /public/plugins/<plugin-id>/;

  • Abnormal accesses to /etc/, /proc/, /var/lib/grafana/ via this endpoint.

Loki queries (examples):

{app="nginx", namespace="edge"} |= "/public/plugins/" |~ "\\.\\.|%2e%2e"
{app="ingress-nginx"} |= "/public/plugins/" |~ "%2f..|/\\.\\."

Specific threat hunting

  • Reading grafana.ini followed by grafana.db from the same IP/ASN;

  • Scanning patterns for common plugin-ids (alertlist, annolist, etc.).


Summarized risk matrix

Scenario

Exposure

Authentication

WAF/Rewrite

Risk

Grafana internet-exposed

High

No requirement

Absent

Critical

Grafana behind Ingress without WAF

Medium

No requirement

Partial

High

Grafana only in-cluster

Low

No requirement

Not applicable

Medium

Updated Grafana + WAF + NP + policies

Low

Present

Low


Checklist for your team

  • Inventory versions of Grafana and update to 8.0.7/8.1.8/8.2.7/8.3.1 or higher;

  • Block ../ in /public/plugins/ on Nginx/Ingress/CDN;

  • Restrict exposure (allowed IPs, mTLS when possible);

  • Apply base NetworkPolicies and Admission Policies;

  • Rotate secret_key and assess KMS for Grafana data;

  • Alerts in Loki for traversal patterns;

  • Assessment of installed plugins (remove what is not necessary) and review of data sources credentials.


References

  • Advisory/fixed product lines by Grafana (Dec/2021). (Grafana Labs)

  • Official technical details and timeline of the 0-day. (Grafana Labs)

  • Reach 8.0.0-beta1 → 8.3.0; vector /public/plugins/<plugin-id>/.... (GitHub)

  • List of useful standard plugin-ids for proof of concept. (SonicWall)

  • Exploit-DB / Metasploit scanner for studying the vector. (exploit-db.com)

  • Secrets encryption in Grafana (DEKs/KEK secret_key) and use of KMS. (Grafana Labs)

  • Analysis of exploitation and usage context as initial access. (SonicWall)


Want to understand how to build a container ecosystem without CVEs? Discover at quor.dev

Shrink your attack surface.

Cut remediation costs.

Reduce your attack surface and the cost of remediation.

With Quor, security becomes your competitive edge. See how in a personalized demo.

Documentation

sales@quor.dev

Powered by Getup