Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Code
4debd2d01a fix(legacy-preset-chart-nvd3): sanitize tooltip HTML built from chart data
Several tooltip generators in this module build HTML strings from chart
data and return them to be rendered via D3 `.html()`, but did not run the
result through DOMPurify the way the sibling generators in the same file
already do. Apply `dompurify.sanitize()` to the returned HTML in
`generateBubbleTooltipContent`, `generateMultiLineTooltipContent`, and the
`tipFactory` annotation tooltip callback so data-derived values are
rendered as text rather than markup.

Adds regression tests asserting that script/handler markup in the
data-derived fields is stripped from the generated tooltip HTML.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:05:03 -07:00
2 changed files with 67 additions and 5 deletions

View File

@@ -162,7 +162,7 @@ export function generateMultiLineTooltipContent(d, xFormatter, yFormatters) {
tooltip += '</tbody></table>';
return tooltip;
return dompurify.sanitize(tooltip);
}
export function generateTimePivotTooltip(d, xFormatter, yFormatter) {
@@ -223,7 +223,7 @@ export function generateBubbleTooltipContent({
s += createHTMLRow(getLabel(sizeField), sizeFormatter(point.size));
s += '</table>';
return s;
return dompurify.sanitize(s);
}
// shouldRemove indicates whether the nvtooltips should be removed from the DOM
@@ -287,9 +287,11 @@ export function tipFactory(layer) {
? layer.descriptionColumns.map(c => d[c])
: Object.values(d);
return `<div><strong>${title}</strong></div><br/><div>${body.join(
', ',
)}</div>`;
return dompurify.sanitize(
`<div><strong>${title}</strong></div><br/><div>${body.join(
', ',
)}</div>`,
);
});
}

View File

@@ -26,6 +26,9 @@ import {
computeYDomain,
getTimeOrNumberFormatter,
formatLabel,
generateBubbleTooltipContent,
generateMultiLineTooltipContent,
tipFactory,
} from '../src/utils';
const DATA = [
@@ -181,4 +184,61 @@ describe('nvd3/utils', () => {
]);
});
});
describe('tooltip HTML sanitization', () => {
const identity = (v: unknown) => v;
test('generateBubbleTooltipContent strips scripts from entity/group', () => {
const html = generateBubbleTooltipContent({
point: {
name: '<img src=x onerror="alert(1)">',
group: '<script>alert(2)</script>',
color: 'red',
x: 1,
y: 2,
size: 3,
},
entity: 'name',
xField: 'x',
yField: 'y',
sizeField: 'size',
xFormatter: identity,
yFormatter: identity,
sizeFormatter: identity,
});
expect(html).not.toContain('onerror');
expect(html).not.toContain('<script>');
});
test('generateMultiLineTooltipContent strips scripts from series keys', () => {
const html = generateMultiLineTooltipContent(
{
value: 'x',
series: [
{ key: '<img src=x onerror="alert(1)">', color: 'red', value: 1 },
],
},
identity,
[identity],
);
expect(html).not.toContain('onerror');
});
test('tipFactory strips scripts from annotation data values', () => {
const tip = tipFactory({
titleColumn: 'title',
name: 'layer',
descriptionColumns: ['desc'],
});
const html = tip.html()({
title: '<img src=x onerror="alert(1)">',
desc: '<script>alert(2)</script>',
});
expect(html).not.toContain('onerror');
expect(html).not.toContain('<script>');
});
});
});