One Plugin Away: Breaking Into Grafana from the Inside

user profile
Security Researcher

Executive Summary

The Cycode research team uncovered two high-impact security vulnerabilities in Grafana plugins, one of which has been assigned CVE-2025-8341. These issues could allow attackers to escalate privileges, extract sensitive secrets, and even pivot into the internal cloud infrastructure.

In this blog, we’ll walk you through how we uncovered these risks, how they work, and how they can be abused. We’ll break down the technical details, show how default configurations can become dangerous, and share actionable best practices we gathered along the way to help you secure your own Grafana instances.

Using the SQLite plugin, an attacker is able to attach directly to Grafana’s internal database and gain full administrative control over every organization, dashboard, and user in the instance. In Grafana’s default configuration, this also exposes all stored data source credentials – such as GitHub personal access tokens and AWS keys – allowing complete compromise of any connected systems. A fix for this issue was introduced in version 3.8.0.

Using the Infinity plugin, an attacker could bypass its allow list mechanism using a maliciously crafted URL and perform a server-side request forgery (SSRF). This vulnerability was fixed in version 3.4.1 and assigned CVE-2025-8341.

 

What is Grafana?

Grafana is one of the most widely adopted observability platforms in modern DevOps and cloud-native environments. It pulls telemetry from dozens of sources – metrics, logs, traces, APIs – and turns them into dashboards that help teams monitor performance, detect failures, and understand complex systems.

To meet the growing complexity of today’s environments, Grafana is built to aggregate and correlate data from many sources. It supports everything from Prometheus and Loki to MySQL, MongoDB, Redis, ClickHouse, different SCMs such as GitHub, GitLab, and cloud services like AWS CloudWatch and Google BigQuery.

To achieve this, Grafana relies on a plugin-based model that connects it to a wide range of external systems. Installing a plugin extends Grafana’s capabilities – enabling it to communicate with third-party services, pull in their data, and, depending on the plugin, interact with APIs, use SDKs, or access the local file system.

Grafana Plugins

Plugins extend Grafana’s core functionality by adding support for new data sources, panels, and applications. These plugins allow Grafana to connect with services it doesn’t support out of the box.

Let’s take a simple example: the GitHub data source plugin. Once installed, it enables Grafana to communicate with GitHub’s API, allowing users to visualize data such as open issues, pull requests, and repository statistics.

What actually happens under the hood when a plugin is installed?

Upon installation, Grafana extracts the plugin into a dedicated directory at /var/lib/grafana/pluginsand executes it, spawning a separate process that runs under the grafana user – the same user responsible for running the main Grafana process. In other words, installing a plugin effectively adds new code that runs with the same system-level permissions as Grafana itself.

Once you configure the GitHub data source with a personal access token, the plugin effectively acts as a bridge, translating Grafana queries into GitHub API requests and returning the results for visualization.

As we began mapping out the plugin architecture, we started asking ourselves the following questions:

  1. What types of plugins exist across the Grafana ecosystem?
  2. What level of access does the grafana user have over the host system?
  3. Can plugins initiate outbound network connections – and if so, where can they reach?
  4. Can we use a plugin to escalate our privileges inside the Grafana instance?
  5. How are data source credentials stored in Grafana?

These questions led us to dig deeper into the mechanics – and potential risks – of Grafana plugins, which we’ll explore in the next section.

The Hidden Risks of Grafana Plugins

After understanding how plugins work and what they can access, we wanted to put our assumptions to the test. We began by mapping out the capabilities of different plugins – focusing on file system access, network access, and their general interaction with the Grafana environment.

To do this, we reviewed several popular plugins under different user permission levels, exploring how their features could be leveraged in various scenarios.

SQLite Plugin

This plugin immediately caught our attention. We recalled that Grafana itself uses an SQLite database by default to store internal data such as users, organizations, secrets, and configurations. Checking the file system, we found that the grafana user – the same user running the plugin process – had read and write access to the internal Grafana database.

Curious to see how far we could take this, we installed the SQLite plugin, pointed it at the Grafana database file, and quickly realized we could query and modify critical data within the Grafana instance.

By examining the database schema, we identified the usertable, which included a column named is_admin. By updating our own user’s record to set is_admin to 1, we escalated our privileges from a limited team admin to a full Grafana administrator. Additionally, we could grant ourselves access to every organization configured in the instance.

With direct access to Grafana’s internal database, we became curious about whether the data sources we had configured earlier were also stored within the same database, and more specifically, how these data source tokens were being stored. Looking into the secrets table, we quickly confirmed that these tokens were indeed stored in the database, but in an encrypted manner.

This brought up the next big question: how does Grafana encrypt these tokens, and can we leverage our current access to decrypt and extract these credentials in plain text?

Grafana’s Secrets Encryption

Grafana uses AES symmetric encryption for the encryption and decryption of data source credentials. The encryption workflow is structured in the following way:

  • Grafana stores an encrypted data key in the data_keys table.
  • This data key is being encrypted and decrypted using a root key that is configured in the Grafana configuration file.
  • Using a decrypted data key, Grafana encrypts and decrypts tokens that are stored in the secrets table.

Reviewing the Grafana configuration, we spotted the root key definition under the [security]section:

[security]
secret_key = SW2YcwTIb9zpOOhoPsMm

Looking into whether this key was unique to our environment or dynamically generated, we discovered that this value is the default encryption key provided by Grafana. This value is included in the official Grafana Docker image, meaning any Container-based or Kubernetes-based Grafana deployment that does not manually override this setting will be using the exact same root encryption key.

From there, we examined the encrypted GitHub token extracted from the secrets table. The token appeared as a long base64-encoded string:

I1pHVnlkR0oxZW5Cb2FuZG5NR0kjKllXVnpMV05tWWcqeENOWnM4ZlGpCf+BKsQ3XgDUAUCyQ7tv0w1peamVktx4X1VssrbHIOqwEZDm3qCPPSeE5PQoxOUqa02YKrAJH7q9z3RCdvkbgxLYwaaDiZ4zVA98lJjuxheCXB9KYn/W+odjQT4gSxcsxby+qdgt0+wSt+6Y23Ifn+zoFNcR1BdDf94w/tFE

After decoding it, we noticed the token follows a specific structure:

#ZGVydGJ1enBoandnMGI#*YWVzLWNmYg*<Encrypted Bytes>

This format uses two predefined delimiters:

  • #to separate the key ID
  • *to separate the encryption algorithm

The key ID itself is also a Base64-encoded value, so we began by decoding it as well:

ZGVydGJ1enBoandnMGI → dertbuzphjwg0b

This value matched an entry in the data_keys table, allowing us to extract the encrypted data key used for this token.

Next, we decoded the algorithm section:

YWVzLWNmYg → aes-cfb

Using the hardcoded root key, we decrypted the data key from the data_keystable, and with the decrypted data key, we proceeded to decrypt the token value from the secrets table.

Since the secrets table contains tokens and credentials for all organizations, this method allowed us to extract all the credentials from the Grafana instance, regardless of organizational boundaries.

This effectively gives us complete control over the Grafana instance – access to all users, organizations, dashboards, plugins and secrets.

 

Infinity Plugin

In addition to exploring filesystem access, we also wanted to investigate plugins with network capabilities. The Infinity plugin allows users to send HTTP requests to any URL and customize those requests with headers, parameters, and payloads.

The first question we had was simple: Can this plugin access internal resources from within the Grafana environment?

To verify this, we created an HTTP request to the GCP metadata service, aiming to retrieve the service account token – a commonly targeted internal resource. The result was clear: we successfully accessed internal endpoints, confirming that the plugin could interact not only with public APIs but also with internal services.

To address this risk, the Infinity plugin includes an optional allow list configuration. This feature limits requests to a predefined set of approved URLs. For our test, we set http://example.com as the only allowed URL. When attempting to send a request to the metadata service again, the plugin blocked the request, returning the following error:

We wanted to understand exactly how this allow list works – so we dug into the plugin’s source code, finding this validation:

if !CanAllowURL(req.URL.String(), settings.AllowedHosts) {
    logger.Debug("url is not in the allowed list. make sure to match the base URL with the settings", "url", req.URL.String())
    return nil, http.StatusUnauthorized, 0, backend.DownstreamError(models.ErrInvalidConfigHostNotAllowed)
}

...

func CanAllowURL(url string, allowedHosts []string) bool {
    allow := false
    if len(allowedHosts) == 0 {
        return true
    }
    for _, host := range allowedHosts {
        if strings.HasPrefix(url, host) {
            return true
        }
    }
    return allow
}

Before executing any request, the plugin calls the CanAllowURL function to determine whether the requested URL is permitted. This function takes the full URL string – req.URL.String() – and checks if it starts with any of the entries in the allowed hosts list.

The problem is that this HasPrefix check only verifies the beginning of the URL, not the actual parsed hostname. This means a URL could appear to start with an allowed host while pointing somewhere entirely different.

URLs can contain special characters that significantly change how they are interpreted. One such character is the @ symbol. According to the URL specification, anything before the @ is treated as credentials (username and password), while everything after it is interpreted as the actual destination host and path.

With that in mind, we crafted a URL that begins with an allowed prefix but actually routes to a different destination:

http://[email protected]/computeMetadata/v1/instance/service-accounts/default/token

This effectively bypassed the plugin’s allow list and enabled access to internal resources that should have been restricted.

Accessing the metadata service is just one example – an attacker could use the Infinity plugin to interact with additional internal services, perform reconnaissance, and potentially escalate their privileges within the environment. This effectively turns Grafana into a pivot point, allowing attackers to compromise internal infrastructure beyond the Grafana instance itself.

Best Practices

During our research into Grafana, we gathered a set of best practices to help harden Grafana deployments. While these recommendations were shaped by the vulnerabilities we uncovered, they follow the same principles as general security hardening – such as overriding insecure default configurations and limiting network/fs access.

Change the Default Encryption Key

By default, Grafana ships with a hardcoded secret key that is embedded in its Docker image. This key is a crucial part of Grafana’s encryption mechanism, responsible for securing sensitive data such as data source tokens and credentials inside the Grafana database. Leaving this default key unchanged allows attackers to decrypt stored secrets if they gain access to the Grafana database.

To mitigate this, you should configure a unique, random encryption key for your Grafana deployment.

Docker Container Deployment

When running Grafana via Docker, you can set a random encryption key using an environment variable:

docker run -d --name=grafana-secure -p 3000:3000 -e "GF_SECURITY_SECRET_KEY=$(openssl rand -base64 32)" grafana/grafana

Kubernetes Deployment

When deploying Grafana on Kubernetes using Helm, you can store the secret key either in a Kubernetes Secret or a dedicated secret manager and reference it during installation. For example:

First, create a Kubernetes Secret:

kubectl create secret generic grafana-secret-key --from-literal=GF_SECURITY_SECRET_KEY=$(openssl rand -base64 32) -n grafana

Then, install Grafana with the secret we just created:

helm install grafana grafana/grafana -n grafana --set envFromSecret=grafana-secret-key

Use an External Grafana Database

Grafana stores its internal database on the local filesystem (/var/lib/grafana/grafana.db). If an attacker gains access to the Grafana server’s file system, they could directly access the Grafana database.

Switching to an external database (such as a remote PostgreSQL or MySQL instance) reduces this risk by removing sensitive data from the Grafana server itself. This adds an extra layer of protection against local file system compromise – as we demonstrated earlier, or through vulnerabilities such as CVE-2021-43798.

Block the SQLite Plugin from Accessing the Grafana Database

Since our reporting on the ability to access Grafana’s internal database through the SQLite plugin, the plugin has introduced a configuration option to block access to specific files.
By setting the GF_PLUGIN_BLOCK_LIST environment variable, you can prevent the plugin from attaching to sensitive files, including Grafana’s own database.

For example:

GF_PLUGIN_BLOCK_LIST="grafana.db"

This configuration ensures that any attempt to connect the SQLite plugin to the Grafana database file will be blocked. A more detailed overview of this feature can be found in the plugin’s official documentation here.

Configure a Strict Allowed Hosts Policy in the Infinity Plugin

Following our disclosure of the bypass in the Infinity plugin’s allow list mechanism, the issue was acknowledged by Grafana, and a fix was released to address the vulnerability.

Without proper restrictions, users across any organization in the Grafana instance could leverage the plugin to send arbitrary HTTP requests, potentially targeting internal services and escalating their privileges.

To reduce this risk, define an explicit list of approved URLs that the plugin is allowed to communicate with. This limits the plugin’s scope and helps prevent misuse that could compromise internal infrastructure.

Network Policy Enforcement

When deploying Grafana in a Kubernetes environment, it’s important to control where the Grafana instance is allowed to communicate with. Without proper network restrictions, a compromised plugin could be abused to interact with internal services – such as metadata endpoints or internal APIs – leading to further lateral movement.

To reduce this risk, apply a Kubernetes network policy to limit Grafana’s egress traffic to only the domains or IP ranges it genuinely needs. You can also restrict access to specific namespaces, ensuring Grafana cannot reach workloads outside its intended scope – even within the cluster.

Additionally, make sure your cluster uses a CNI plugin that enforces network policies. Without a proper CNI, defined NetworkPolicy objects won’t actually be enforced.

 

Responsible Disclosure

Throughout our research, we worked closely with both Grafana and plugin maintainers to report the issues we uncovered. Both were quick to engage and showed a strong commitment to improving the platform’s security.

As part of our disclosure process, we reported the SQLite plugin issue to its maintainer. In response, a fix has been applied in version 3.8.0, adding a configuration option to block access to specific file paths – preventing misuse like the one we demonstrated.

We disclosed the allow list bypass in the Infinity plugin to the Grafana team. They confirmed the vulnerability and released a fix in version 3.4.1, which correctly validates the parsed hostname of the provided URL. This vulnerability was assigned CVE-2025-8341.

How Cycode Can Help?

Cycode’s AI-Native Application Security Platform integrates across every stage of the SDLC and CI/CD pipeline, leveraging context from code to runtime to identify, prioritize, and fix software risks and misconfigurations before attackers can exploit them. 

For example, by integrating with Kubernetes clusters, Cycode can detect which clusters are hosting Grafana, which of them are configured with default admin credentials, identify those exposed to the internet, and trace those issues back to their origin repository. This enables teams to quickly pinpoint and resolve risky configurations.

Ready to see how Cycode can help secure your entire software supply chain? Get a demo today.