VYPR
Moderate severityNVD Advisory· Published Feb 12, 2026· Updated Apr 24, 2026

XSS in Grafana Explore stack trace

CVE-2025-41117

Description

Stack traces in Grafana's Explore Traces view can be rendered as raw HTML, and thus inject malicious JavaScript in the browser. This would require malicious JavaScript to be entered into the stack trace field.

Only datasources with the Jaeger HTTP API appear to be affected; Jaeger gRPC and Tempo do not appear affected whatsoever.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CVE-2025-41117 is a stored cross-site scripting vulnerability in Grafana's Explore Traces view, where stack traces from Jaeger HTTP API datasources are rendered as raw HTML without sanitization.

Vulnerability

Overview CVE-2025-41117 is a stored cross-site scripting (XSS) vulnerability in Grafana's Explore Traces view. The root cause is that stack trace data retrieved from datasources using the Jaeger HTTP API is rendered as raw HTML in the TraceView component, without proper sanitization of user-controlled values [1]. The vulnerability exists because the KeyValuesTable component inserted stack trace content directly into the DOM using dangerouslySetInnerHTML without first sanitizing the input, as shown in the GitHub commit patches [3][4].

Exploitation

Prerequisites To exploit this vulnerability, an attacker must first introduce malicious JavaScript into a stack trace field that is stored and later retrieved by a Jaeger HTTP API datasource. This could happen if a user or automated process ingests a trace containing a maliciously crafted stack trace into Jaeger gRPC and Tempo datasources are not affected, as they do not expose the same rendering path [1]. The attack does not require authentication to Grafana authentication if the data source itself accepts unauthenticated writes, but an authenticated user must view the affected trace in the Explore Traces Explore view for the payload to execute.

Impact

Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of the victim's browser session. This could lead to session hijacking, exfiltration of dashboard data, or other malicious actions within the Grafana instance. Since the payload is stored in trace data, it affects any user who views the malicious trace in the Explore Traces interface.

Mitigation

Status Grafana has addressed the vulnerability by introducing HTML sanitization using DOMPurify in the KeyValuesTable component. The fix is included in versions 12.2.5 and 12.3.3, as evidenced by the referenced commits [3][4]. Users should upgrade to these versions or later to eliminate the risk. No workaround is available besides upgrading or avoiding usage of Jaeger HTTP API datasources.

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/grafana/grafanaGo
>= 12.2.0, < 12.2.512.2.5
github.com/grafana/grafanaGo
>= 12.3.0, < 12.3.312.3.3

Affected products

3
  • Grafana/Grafanallm-fuzzy
  • Grafana/grafana/grafanav5
    Range: 12.2.0
  • Grafana/grafana/grafana-enterprisev5
    Range: 12.2.0

Patches

3
8dfa64469428

Security: Sanitize TraceView html (#117853)

https://github.com/grafana/grafanaMariell HoversholmFeb 11, 2026via ghsa
1 file changed · +8 11
  • public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx+8 11 modified
    @@ -14,6 +14,7 @@
     
     import { css } from '@emotion/css';
     import cx from 'classnames';
    +import DOMPurify from 'dompurify';
     import { PropsWithChildren } from 'react';
     
     import { GrafanaTheme2, PluginExtensionLink, TraceKeyValuePair } from '@grafana/data';
    @@ -126,22 +127,18 @@ export default function KeyValuesTable(props: KeyValuesTableProps) {
           <table className={styles.table}>
             <tbody className={styles.body}>
               {data.map((row, i) => {
    -            let markup = { __html: '' };
    +            let html = '';
                 if (row.type === 'code') {
    -              markup = {
    -                __html: `<pre style="border: none; background: none">${row.value}</pre>`,
    -              };
    +              html = `<pre style="border: none; background: none">${row.value}</pre>`;
                 } else if (row.type === 'text') {
    -              markup = {
    -                __html: `<span style="white-space: pre-wrap;">${row.value}</span>`,
    -              };
    +              html = `<span style="white-space: pre-wrap;">${row.value}</span>`;
                 } else {
    -              markup = {
    -                __html: jsonMarkup(parseIfComplexJson(row.value)),
    -              };
    +              html = jsonMarkup(parseIfComplexJson(row.value));
                 }
     
    -            const jsonTable = <div className={styles.jsonTable} dangerouslySetInnerHTML={markup} />;
    +            const jsonTable = (
    +              <div className={styles.jsonTable} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />
    +            );
                 const links = linksGetter?.(data, i);
                 let valueMarkup;
                 if (links && links.length) {
    
ecff0d88680c

[release-12.2.5] Security(TraceView): Sanitize html (#117867)

https://github.com/grafana/grafanaMariell HoversholmFeb 11, 2026via ghsa
1 file changed · +8 11
  • public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx+8 11 modified
    @@ -14,6 +14,7 @@
     
     import { css } from '@emotion/css';
     import cx from 'classnames';
    +import DOMPurify from 'dompurify';
     import { PropsWithChildren } from 'react';
     
     import { GrafanaTheme2, PluginExtensionLink, TraceKeyValuePair } from '@grafana/data';
    @@ -128,22 +129,18 @@ export default function KeyValuesTable(props: KeyValuesTableProps) {
           <table className={styles.table}>
             <tbody className={styles.body}>
               {data.map((row, i) => {
    -            let markup = { __html: '' };
    +            let html = '';
                 if (row.type === 'code') {
    -              markup = {
    -                __html: `<pre style="border: none; background: none">${row.value}</pre>`,
    -              };
    +              html = `<pre style="border: none; background: none">${row.value}</pre>`;
                 } else if (row.type === 'text') {
    -              markup = {
    -                __html: `<span style="white-space: pre-wrap;">${row.value}</span>`,
    -              };
    +              html = `<span style="white-space: pre-wrap;">${row.value}</span>`;
                 } else {
    -              markup = {
    -                __html: jsonMarkup(parseIfComplexJson(row.value)),
    -              };
    +              html = jsonMarkup(parseIfComplexJson(row.value));
                 }
     
    -            const jsonTable = <div className={styles.jsonTable} dangerouslySetInnerHTML={markup} />;
    +            const jsonTable = (
    +              <div className={styles.jsonTable} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />
    +            );
                 const links = linksGetter?.(data, i);
                 let valueMarkup;
                 if (links && links.length) {
    
4f624a5a0140

[release-12.3.3] Security(TraceView): Sanitize html (#117866)

https://github.com/grafana/grafanaMariell HoversholmFeb 11, 2026via ghsa
1 file changed · +8 11
  • public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx+8 11 modified
    @@ -14,6 +14,7 @@
     
     import { css } from '@emotion/css';
     import cx from 'classnames';
    +import DOMPurify from 'dompurify';
     import { PropsWithChildren } from 'react';
     
     import { GrafanaTheme2, PluginExtensionLink, TraceKeyValuePair } from '@grafana/data';
    @@ -128,22 +129,18 @@ export default function KeyValuesTable(props: KeyValuesTableProps) {
           <table className={styles.table}>
             <tbody className={styles.body}>
               {data.map((row, i) => {
    -            let markup = { __html: '' };
    +            let html = '';
                 if (row.type === 'code') {
    -              markup = {
    -                __html: `<pre style="border: none; background: none">${row.value}</pre>`,
    -              };
    +              html = `<pre style="border: none; background: none">${row.value}</pre>`;
                 } else if (row.type === 'text') {
    -              markup = {
    -                __html: `<span style="white-space: pre-wrap;">${row.value}</span>`,
    -              };
    +              html = `<span style="white-space: pre-wrap;">${row.value}</span>`;
                 } else {
    -              markup = {
    -                __html: jsonMarkup(parseIfComplexJson(row.value)),
    -              };
    +              html = jsonMarkup(parseIfComplexJson(row.value));
                 }
     
    -            const jsonTable = <div className={styles.jsonTable} dangerouslySetInnerHTML={markup} />;
    +            const jsonTable = (
    +              <div className={styles.jsonTable} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />
    +            );
                 const links = linksGetter?.(data, i);
                 let valueMarkup;
                 if (links && links.length) {
    

Vulnerability mechanics

Synthesis attempt was rejected by the grounding validator. Re-run pending.

References

7

News mentions

0

No linked articles in our index yet.