VYPR
Moderate severityNVD Advisory· Published Mar 2, 2023· Updated Jan 28, 2026

Stored XSS in Grafana Text plugin

CVE-2023-22462

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.

PackageAffected versionsPatched versions
github.com/grafana/grafanaGo
>= 9.2.0, < 9.2.109.2.10
github.com/grafana/grafanaGo
>= 9.3.0, < 9.3.49.3.4

Affected products

1

Patches

1
db83d5f398ca

[v9.3.x] Plugins: Fix plugin query help markdown (#60907)

https://github.com/grafana/grafanaGrot (@grafanabot)Jan 3, 2023via ghsa
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>&apos;Error occurred when loading help&apos;</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

News mentions

0

No linked articles in our index yet.