[v8.5.x] SVG: Add dompurify preprocessor step (#707)

* SVG: Add dompurify preprocessor step (#698)

* add sanitized SVG component

(cherry picked from commit dbbe819368f507b9493b7a3cf6e080a5dd752b3e)

* Remove unused import to fix build

* Fix lint issues

* Remove additional class handling as not needed

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Nathan Marrs
2023-01-09 17:02:38 +01:00
committed by Horst Gutmann
co-authored by Ryan McKinley
parent 62dd2999f0
commit 5f3ca5be07
9 changed files with 62 additions and 12 deletions
+2
View File
@@ -122,6 +122,7 @@
"@types/d3-force": "^2.1.0",
"@types/d3-scale-chromatic": "1.3.1",
"@types/debounce-promise": "3.1.4",
"@types/dompurify": "^2",
"@types/enzyme": "3.10.11",
"@types/enzyme-adapter-react-16": "1.0.6",
"@types/eslint": "8.4.1",
@@ -306,6 +307,7 @@
"dangerously-set-html-content": "1.0.9",
"date-fns": "2.28.0",
"debounce-promise": "3.1.2",
"dompurify": "^2.4.1",
"emotion": "11.0.0",
"eventemitter3": "4.0.7",
"fast-deep-equal": "^3.1.3",
@@ -0,0 +1,18 @@
import * as DOMPurify from 'dompurify';
import React from 'react';
import SVG, { Props } from 'react-inlinesvg';
export const SanitizedSVG = (props: Props) => {
return <SVG {...props} cacheRequests={true} preProcessor={getCleanSVG} />;
};
let cache = new Map<string, string>();
function getCleanSVG(code: string): string {
let clean = cache.get(code);
if (!clean) {
clean = DOMPurify.sanitize(code, { USE_PROFILES: { svg: true, svgFilters: true } });
cache.set(code, clean);
}
return clean;
}
+2 -2
View File
@@ -1,8 +1,8 @@
import { css } from '@emotion/css';
import { isString } from 'lodash';
import React, { CSSProperties } from 'react';
import SVG from 'react-inlinesvg';
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
import {
ColorDimensionConfig,
ResourceDimensionConfig,
@@ -57,7 +57,7 @@ export function IconDisplay(props: CanvasElementProps) {
};
return (
<SVG
<SanitizedSVG
onClick={onClick}
src={data.path}
width={width}
@@ -1,11 +1,11 @@
import { css, cx } from '@emotion/css';
import React, { memo, CSSProperties } from 'react';
import SVG from 'react-inlinesvg';
import AutoSizer from 'react-virtualized-auto-sizer';
import { areEqual, FixedSizeGrid as Grid } from 'react-window';
import { GrafanaTheme2 } from '@grafana/data';
import { useTheme2, stylesFactory } from '@grafana/ui';
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
import { ResourceItem } from './FolderPickerTab';
@@ -38,7 +38,7 @@ function Cell(props: CellProps) {
onClick={() => onChange(card.value)}
>
{card.imgUrl.endsWith('.svg') ? (
<SVG src={card.imgUrl} className={styles.img} />
<SanitizedSVG src={card.imgUrl} className={styles.img} />
) : (
<img src={card.imgUrl} className={styles.img} />
)}
@@ -1,6 +1,5 @@
import { css } from '@emotion/css';
import React, { createRef } from 'react';
import SVG from 'react-inlinesvg';
import { GrafanaTheme2 } from '@grafana/data';
import {
@@ -15,6 +14,7 @@ import {
useTheme2,
} from '@grafana/ui';
import { closePopover } from '@grafana/ui/src/utils/closePopover';
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
import { getPublicOrAbsoluteUrl } from '../resource';
import { MediaType, ResourceFolderName, ResourcePickerSize } from '../types';
@@ -56,7 +56,7 @@ export const ResourcePicker = (props: Props) => {
const renderSmallResourcePicker = () => {
if (value && sanitizedSrc) {
return <SVG src={sanitizedSrc} className={styles.icon} style={{ ...colorStyle }} />;
return <SanitizedSVG src={sanitizedSrc} className={styles.icon} style={{ ...colorStyle }} />;
} else {
return (
<LinkButton variant="primary" fill="text" size="sm">
@@ -73,7 +73,7 @@ export const ResourcePicker = (props: Props) => {
value={name}
placeholder={placeholder}
readOnly={true}
prefix={sanitizedSrc && <SVG src={sanitizedSrc} className={styles.icon} style={{ ...colorStyle }} />}
prefix={sanitizedSrc && <SanitizedSVG src={sanitizedSrc} className={styles.icon} style={{ ...colorStyle }} />}
suffix={<Button icon="times" variant="secondary" fill="text" size="sm" onClick={onClear} />}
/>
</InlineField>
@@ -1,9 +1,9 @@
import { css } from '@emotion/css';
import React, { Dispatch, SetStateAction } from 'react';
import SVG from 'react-inlinesvg';
import { GrafanaTheme2 } from '@grafana/data';
import { Field, Input, Label, useStyles2 } from '@grafana/ui';
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
import { getPublicOrAbsoluteUrl } from '../resource';
import { MediaType } from '../types';
@@ -33,8 +33,10 @@ export const URLPickerTab = (props: Props) => {
<div className={styles.iconContainer}>
<Field label="Preview">
<div className={styles.iconPreview}>
{mediaType === MediaType.Icon && <SVG src={imgSrc} className={styles.img} />}
{mediaType === MediaType.Image && newValue && <img src={imgSrc} className={styles.img} />}
{mediaType === MediaType.Icon && <SanitizedSVG src={imgSrc} className={styles.img} />}
{mediaType === MediaType.Image && newValue && (
<img src={imgSrc} alt="Preview of the selected URL" className={styles.img} />
)}
</div>
</Field>
<Label>{shortName}</Label>
@@ -6,8 +6,8 @@ import { config } from 'app/core/config';
import { DimensionSupplier } from 'app/features/dimensions';
import { getThresholdItems } from 'app/plugins/panel/state-timeline/utils';
import { getMinMaxAndDelta } from '@grafana/data/src/field/scale';
import SVG from 'react-inlinesvg';
import { StyleConfigState } from '../../style/types';
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
export interface MarkersLegendProps {
size?: DimensionSupplier<number>;
@@ -32,7 +32,7 @@ export function MarkersLegend(props: MarkersLegendProps) {
return (
<div className={style.infoWrap}>
<div className={style.fixedColorContainer}>
<SVG
<SanitizedSVG
src={`public/${symbol}`}
className={style.legendSymbol}
title={'Symbol'}
@@ -1,3 +1,4 @@
import * as DOMPurify from 'dompurify';
import { Fill, RegularShape, Stroke, Circle, Style, Icon, Text } from 'ol/style';
import tinycolor from 'tinycolor2';
@@ -227,6 +228,8 @@ async function prepareSVG(url: string, size?: number): Promise<string> {
return res.text();
})
.then((text) => {
text = DOMPurify.sanitize(text, { USE_PROFILES: { svg: true, svgFilters: true } });
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'image/svg+xml');
const svg = doc.getElementsByTagName('svg')[0];
+25
View File
@@ -9689,6 +9689,15 @@ __metadata:
languageName: node
linkType: hard
"@types/dompurify@npm:^2":
version: 2.4.0
resolution: "@types/dompurify@npm:2.4.0"
dependencies:
"@types/trusted-types": "*"
checksum: b48cd81e997794ebc390c7c5bef1a67ec14a6f2f0521973e07e06af186c7583abe114d94d24868c0632b9573f5bd77131a4b76f3fffdf089ba99a4e53dd46c39
languageName: node
linkType: hard
"@types/enzyme-adapter-react-16@npm:1.0.6":
version: 1.0.6
resolution: "@types/enzyme-adapter-react-16@npm:1.0.6"
@@ -10871,6 +10880,13 @@ __metadata:
languageName: node
linkType: hard
"@types/trusted-types@npm:*":
version: 2.0.2
resolution: "@types/trusted-types@npm:2.0.2"
checksum: 3371eef5f1c50e1c3c07a127c1207b262ba65b83dd167a1c460fc1b135a3fb0c97b9f508efebd383f239cc5dd5b7169093686a692a501fde9c3f7208657d9b0d
languageName: node
linkType: hard
"@types/uglify-js@npm:*":
version: 3.13.1
resolution: "@types/uglify-js@npm:3.13.1"
@@ -17568,6 +17584,13 @@ __metadata:
languageName: node
linkType: hard
"dompurify@npm:^2.4.1":
version: 2.4.1
resolution: "dompurify@npm:2.4.1"
checksum: 1169177465b3cbb25a44322937fba549f6c4e1a91b83245d144471be26619c835cccf0f8e20aa78c25ac11a06efd17cc1b9db9cacadceb78a4c08a1029eafee5
languageName: node
linkType: hard
"domutils@npm:^2.5.2, domutils@npm:^2.6.0, domutils@npm:^2.7.0":
version: 2.8.0
resolution: "domutils@npm:2.8.0"
@@ -20567,6 +20590,7 @@ __metadata:
"@types/d3-force": ^2.1.0
"@types/d3-scale-chromatic": 1.3.1
"@types/debounce-promise": 3.1.4
"@types/dompurify": ^2
"@types/enzyme": 3.10.11
"@types/enzyme-adapter-react-16": 1.0.6
"@types/eslint": 8.4.1
@@ -20649,6 +20673,7 @@ __metadata:
dangerously-set-html-content: 1.0.9
date-fns: 2.28.0
debounce-promise: 3.1.2
dompurify: ^2.4.1
emotion: 11.0.0
enzyme: 3.11.0
enzyme-to-json: 3.6.2