mirror of
https://github.com/prometheus/prometheus.git
synced 2026-04-18 20:21:05 +02:00
UI: Fix stored XSS via unescaped metric names and labels
Metric names, label names, and label values containing HTML/JavaScript were inserted into `innerHTML` without escaping in several UI code paths, enabling stored XSS attacks via crafted metrics. This mostly becomes exploitable in Prometheus 3.x, since it defaults to allowing any UTF-8 characters in metric and label names. Apply `escapeHTML()` to all user-controlled values before innerHTML insertion in: * Mantine UI chart tooltip * Old React UI chart tooltip * Old React UI metrics explorer fuzzy search * Old React UI heatmap tooltip See https://github.com/prometheus/prometheus/security/advisories/GHSA-vffh-x6r8-xx99 Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
1bd2f3a9fd
commit
fddbccf79b
@ -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) {
|
||||
|
||||
@ -118,10 +118,10 @@ 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>`;
|
||||
|
||||
@ -129,7 +129,7 @@ export const getOptions = (stacked: boolean, useLocalTime: boolean): jquery.flot
|
||||
<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)}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>`;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user