VYPR
Medium severity6.1NVD Advisory· Published Apr 15, 2026· Updated Apr 22, 2026

CVE-2026-40179

CVE-2026-40179

Description

Prometheus is an open-source monitoring system and time series database. Versions 3.0 through 3.5.1 and 3.6.0 through 3.11.1 have stored cross-site scripting vulnerabilities in multiple components of the Prometheus web UI where metric names and label values are injected into innerHTML without escaping. In both the Mantine UI and old React UI, chart tooltips on the Graph page render metric names containing HTML/JavaScript without sanitization. In the old React UI, the Metric Explorer fuzzy search results use dangerouslySetInnerHTML without escaping, and heatmap cell tooltips interpolate le label values without sanitization. With Prometheus v3.x defaulting to UTF-8 metric and label name validation, characters like <, >, and " are now valid in metric names and labels. An attacker who can inject metrics via a compromised scrape target, remote write, or OTLP receiver endpoint can execute arbitrary JavaScript in the browser of any Prometheus user who views the metric in the Graph UI, potentially enabling configuration exfiltration, data deletion, or Prometheus shutdown depending on enabled flags. This issue has been fixed in versions 3.5.2 and 3.11.2. If developers are unable to immediately update, the following workarounds are recommended: ensure that the remote write receiver (--web.enable-remote-write-receiver) and the OTLP receiver (--web.enable-otlp-receiver) are not exposed to untrusted sources; verify that all scrape targets are trusted and not under attacker control; avoid enabling admin or mutating API endpoints (e.g., --web.enable-admin-api or --web.enable-lifecycle) in environments where untrusted data may be ingested; and refrain from clicking untrusted links, particularly those containing functions such as label_replace, as they may generate poisoned label names and values.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/prometheus/prometheusGo
>= 3.0.0, <= 3.5.1
github.com/prometheus/prometheusGo
>= 3.6.0, <= 3.11.1
github.com/prometheus/prometheusGo
< 0.311.2-0.20260410083055-07c6232d159b0.311.2-0.20260410083055-07c6232d159b

Affected products

1

Patches

1
07c6232d159b

Merge commit from fork

https://github.com/prometheus/prometheusJulius VolzApr 10, 2026via ghsa
4 files changed · +16 15
  • web/ui/mantine-ui/src/pages/query/uPlotChartHelpers.ts+10 10 modified
    @@ -76,7 +76,7 @@ const formatLabels = (labels: { [key: string]: string }): string => `
                     .filter((k) => k !== "__name__")
                     .map(
                       (k) =>
    -                    `<div><strong>${escapeHTML(k)}</strong>: ${escapeHTML(labels[k])}</div>`
    +                    `<div><strong>${escapeHTML(k)}</strong>: ${escapeHTML(labels[k])}</div>`,
                     )
                     .join("")}
                 </div>`;
    @@ -153,7 +153,7 @@ const tooltipPlugin = (useLocalTime: boolean, data: AlignedData) => {
                 <div class="date">${formatTimestamp(ts, useLocalTime)}</div>
                 <div class="series-value">
                   <span class="detail-swatch" style="background-color: ${color}"></span>
    -              <span>${labels.__name__ ? labels.__name__ + ": " : " "}<strong>${value}</strong></span>
    +              <span>${labels.__name__ ? escapeHTML(labels.__name__) + ": " : " "}<strong>${value}</strong></span>
                 </div>
                 ${formatLabels(labels)}
               `.trimEnd();
    @@ -193,7 +193,7 @@ const autoPadLeft = (
       u: uPlot,
       values: string[],
       axisIdx: number,
    -  cycleNum: number
    +  cycleNum: number,
     ) => {
       const axis = u.axes[axisIdx];
     
    @@ -208,7 +208,7 @@ const autoPadLeft = (
       // Find longest tick text.
       const longestVal = (values ?? []).reduce(
         (acc, val) => (val.length > acc.length ? val : acc),
    -    ""
    +    "",
       );
     
       if (longestVal != "") {
    @@ -228,7 +228,7 @@ const onlyDrawPointsForDisconnectedSamplesFilter = (
       u: uPlot,
       seriesIdx: number,
       show: boolean,
    -  gaps?: null | number[][]
    +  gaps?: null | number[][],
     ) => {
       const filtered = [];
     
    @@ -287,7 +287,7 @@ export const getUPlotOptions = (
       useLocalTime: boolean,
       yAxisMin: number | null,
       light: boolean,
    -  onSelectRange: (_start: number, _end: number) => void
    +  onSelectRange: (_start: number, _end: number) => void,
     ): uPlot.Options => ({
       width: width - 30,
       height: 550,
    @@ -314,7 +314,7 @@ export const getUPlotOptions = (
         markers: {
           fill: (
             _u: uPlot,
    -        seriesIdx: number
    +        seriesIdx: number,
           ): CSSStyleDeclaration["borderColor"] =>
             // Because the index here is coming from uPlot, we need to subtract 1. Series 0
             // represents the X axis, so we need to skip it.
    @@ -411,7 +411,7 @@ export const getUPlotOptions = (
             // @ts-expect-error - uPlot doesn't have a field for labels, but we just attach some anyway.
             labels: r.metric,
             stroke: getSeriesColor(idx, light),
    -      })
    +      }),
         ),
       ],
       hooks: {
    @@ -421,7 +421,7 @@ export const getUPlotOptions = (
             const leftVal = self.posToVal(self.select.left, "x");
             const rightVal = Math.max(
               self.posToVal(self.select.left + self.select.width, "x"),
    -          leftVal + 1
    +          leftVal + 1,
             );
     
             onSelectRange(leftVal, rightVal);
    @@ -441,7 +441,7 @@ export const getUPlotData = (
       inputData: RangeSamples[],
       startTime: number,
       endTime: number,
    -  resolution: number
    +  resolution: number,
     ): uPlot.AlignedData => {
       const timeData: number[] = [];
       for (let t = startTime; t <= endTime; t += resolution) {
    
  • web/ui/react-app/src/pages/graph/GraphHelpers.ts+3 3 modified
    @@ -118,18 +118,18 @@ export const getOptions = (stacked: boolean, useLocalTime: boolean): jquery.flot
             const formatLabels = (labels: { [key: string]: string }): string => `
                 <div class="labels">
                   ${Object.keys(labels).length === 0 ? '<div class="mb-1 font-italic">no labels</div>' : ''}
    -              ${labels['__name__'] ? `<div class="mb-1"><strong>${labels['__name__']}</strong></div>` : ''}
    +              ${labels['__name__'] ? `<div class="mb-1"><strong>${escapeHTML(labels['__name__'])}</strong></div>` : ''}
                   ${Object.keys(labels)
                     .filter((k) => k !== '__name__')
    -                .map((k) => `<div class="mb-1"><strong>${k}</strong>: ${escapeHTML(labels[k])}</div>`)
    +                .map((k) => `<div class="mb-1"><strong>${escapeHTML(k)}</strong>: ${escapeHTML(labels[k])}</div>`)
                     .join('')}
                 </div>`;
     
             return `
                 <div class="date">${dateTime.format('YYYY-MM-DD HH:mm:ss Z')}</div>
                 <div>
                   <span class="detail-swatch" style="background-color: ${color}"></span>
    -              <span>${labels.__name__ || 'value'}: <strong>${yval}</strong></span>
    +              <span>${labels.__name__ ? escapeHTML(labels.__name__) : 'value'}: <strong>${yval}</strong></span>
                 </div>
                 <div class="mt-2 mb-1 font-weight-bold">${'seriesLabels' in both ? 'Trace exemplar:' : 'Series:'}</div>
                 ${formatLabels(labels)}
    
  • web/ui/react-app/src/pages/graph/MetricsExplorer.tsx+1 1 modified
    @@ -2,7 +2,7 @@ import React, { Component, ChangeEvent } from 'react';
     import { Modal, ModalBody, ModalHeader, Input } from 'reactstrap';
     import { Fuzzy, FuzzyResult } from '@nexucis/fuzzy';
     
    -const fuz = new Fuzzy({ pre: '<strong>', post: '</strong>', shouldSort: true });
    +const fuz = new Fuzzy({ pre: '<strong>', post: '</strong>', shouldSort: true, escapeHTML: true });
     
     interface MetricsExplorerProps {
       show: boolean;
    
  • web/ui/react-app/src/vendor/flot/jquery.flot.heatmap.js+2 1 modified
    @@ -6,6 +6,7 @@ See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3384 for more deta
     
     import moment from 'moment-timezone';
     import {formatValue} from "../../pages/graph/GraphHelpers";
    +import {escapeHTML} from '../../utils';
     
     const TOOLTIP_ID = 'heatmap-tooltip';
     const GRADIENT_STEPS = 16;
    @@ -82,7 +83,7 @@ const GRADIENT_STEPS = 16;
         tooltip.className = cssClass;
     
         const timeHtml = `<div class="date">${dateTime.join('<br>')}</div>`
    -    const labelHtml = `<div>Bucket: ${label || 'value'}</div>`
    +    const labelHtml = `<div>Bucket: ${label ? escapeHTML(label) : 'value'}</div>`
         const valueHtml = `<div>Value: <strong>${value}</strong></div>`
         tooltip.innerHTML = `<div>${timeHtml}<div>${labelHtml}${valueHtml}</div></div>`;
     
    

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

5

News mentions

3