Stored XSS in Grafana Text plugin
Description
Grafana is an open-source platform for monitoring and observability. On 2023-01-01 during an internal audit of Grafana, a member of the security team found a stored XSS vulnerability affecting the core plugin "Text". The stored XSS vulnerability requires several user interactions in order to be fully exploited. The vulnerability was possible due to React's render cycle that will pass though the unsanitized HTML code, but in the next cycle the HTML is cleaned up and saved in Grafana's database. An attacker needs to have the Editor role in order to change a Text panel to include JavaScript. Another user needs to edit the same Text panel, and click on "Markdown" or "HTML" for the code to be executed. This means that vertical privilege escalation is possible, where a user with Editor role can change to a known password for a user having Admin role if the user with Admin role executes malicious JavaScript viewing a dashboard. This issue has been patched in versions 9.2.10 and 9.3.4.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/grafana/grafanaGo | >= 9.2.0, < 9.2.10 | 9.2.10 |
github.com/grafana/grafanaGo | >= 9.3.0, < 9.3.4 | 9.3.4 |
Affected products
1Patches
1db83d5f398ca[v9.3.x] Plugins: Fix plugin query help markdown (#60907)
3 files changed · +19 −70
pkg/api/plugins.go+1 −1 modified@@ -270,7 +270,7 @@ func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response // fallback try readme if len(content) == 0 { - content, err = hs.pluginMarkdown(c.Req.Context(), pluginID, "help") + content, err = hs.pluginMarkdown(c.Req.Context(), pluginID, "readme") if err != nil { return response.Error(501, "Could not get markdown file", err) }
public/app/core/components/PluginHelp/PluginHelp.tsx+17 −68 modified@@ -1,83 +1,32 @@ -import React, { PureComponent } from 'react'; +import React from 'react'; +import { useAsync } from 'react-use'; import { renderMarkdown } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; +import { LoadingPlaceholder } from '@grafana/ui'; interface Props { - plugin: { - name: string; - id: string; - }; - type: string; + pluginId: string; } -interface State { - isError: boolean; - isLoading: boolean; - help: string; -} +export function PluginHelp({ pluginId }: Props) { + const { value, loading, error } = useAsync(async () => { + return getBackendSrv().get(`/api/plugins/${pluginId}/markdown/query_help`); + }, []); -export class PluginHelp extends PureComponent<Props, State> { - state = { - isError: false, - isLoading: false, - help: '', - }; + const renderedMarkdown = renderMarkdown(value); - componentDidMount(): void { - this.loadHelp(); + if (loading) { + return <LoadingPlaceholder text="Loading help..." />; } - constructPlaceholderInfo() { - return 'No plugin help or readme markdown file was found'; + if (error) { + return <h3>An error occurred when loading help.</h3>; } - loadHelp = () => { - const { plugin, type } = this.props; - this.setState({ isLoading: true }); - - getBackendSrv() - .get(`/api/plugins/${plugin.id}/markdown/${type}`) - .then((response: string) => { - const helpHtml = renderMarkdown(response); - - if (response === '' && type === 'help') { - this.setState({ - isError: false, - isLoading: false, - help: this.constructPlaceholderInfo(), - }); - } else { - this.setState({ - isError: false, - isLoading: false, - help: helpHtml, - }); - } - }) - .catch(() => { - this.setState({ - isError: true, - isLoading: false, - }); - }); - }; - - render() { - const { type } = this.props; - const { isError, isLoading, help } = this.state; - - if (isLoading) { - return <h2>Loading help...</h2>; - } - - if (isError) { - return <h3>'Error occurred when loading help'</h3>; - } - - if (type === 'panel_help' && help === '') { - } - - return <div className="markdown-html" dangerouslySetInnerHTML={{ __html: help }} />; + if (value === '') { + return <h3>No query help could be found.</h3>; } + + return <div className="markdown-html" dangerouslySetInnerHTML={{ __html: renderedMarkdown }} />; }
public/app/features/query/components/QueryGroup.tsx+1 −1 modified@@ -364,7 +364,7 @@ export class QueryGroup extends PureComponent<Props, State> { {this.renderAddQueryRow(dsSettings, styles)} {isHelpOpen && ( <Modal title="Data source help" isOpen={true} onDismiss={this.onCloseHelp}> - <PluginHelp plugin={dsSettings.meta} type="query_help" /> + <PluginHelp pluginId={dsSettings.meta.id} /> </Modal> )} </>
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-7rqg-hjwc-6mjfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-22462ghsaADVISORY
- github.com/grafana/grafana/commit/db83d5f398caffe35c5846cfa7727d1a2a414165ghsax_refsource_MISCWEB
- github.com/grafana/grafana/security/advisories/GHSA-7rqg-hjwc-6mjfghsax_refsource_CONFIRMWEB
- grafana.com/blog/2023/02/28/grafana-security-release-new-versions-with-security-fixes-for-cve-2023-0594-cve-2023-0507-and-cve-2023-22462ghsaWEB
- grafana.com/blog/2023/02/28/grafana-security-release-new-versions-with-security-fixes-for-cve-2023-0594-cve-2023-0507-and-cve-2023-22462/mitrex_refsource_MISC
- security.netapp.com/advisory/ntap-20230413-0004ghsaWEB
- security.netapp.com/advisory/ntap-20230413-0004/mitre
News mentions
0No linked articles in our index yet.