diff --git a/package.json b/package.json index 43563833fa3..f77c0b94410 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/app/core/components/SVG/SanitizedSVG.tsx b/public/app/core/components/SVG/SanitizedSVG.tsx new file mode 100644 index 00000000000..9a2628cd63d --- /dev/null +++ b/public/app/core/components/SVG/SanitizedSVG.tsx @@ -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 ; +}; + +let cache = new Map(); + +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; +} diff --git a/public/app/features/canvas/elements/icon.tsx b/public/app/features/canvas/elements/icon.tsx index ea785302161..9f3513b47aa 100644 --- a/public/app/features/canvas/elements/icon.tsx +++ b/public/app/features/canvas/elements/icon.tsx @@ -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 ( - onChange(card.value)} > {card.imgUrl.endsWith('.svg') ? ( - + ) : ( )} diff --git a/public/app/features/dimensions/editors/ResourcePicker.tsx b/public/app/features/dimensions/editors/ResourcePicker.tsx index 7bc55df26b7..06cc2b89bbb 100644 --- a/public/app/features/dimensions/editors/ResourcePicker.tsx +++ b/public/app/features/dimensions/editors/ResourcePicker.tsx @@ -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 ; + return ; } else { return ( @@ -73,7 +73,7 @@ export const ResourcePicker = (props: Props) => { value={name} placeholder={placeholder} readOnly={true} - prefix={sanitizedSrc && } + prefix={sanitizedSrc && } suffix={} /> diff --git a/public/app/features/dimensions/editors/URLPickerTab.tsx b/public/app/features/dimensions/editors/URLPickerTab.tsx index 5402d2e6112..51504e36dd2 100644 --- a/public/app/features/dimensions/editors/URLPickerTab.tsx +++ b/public/app/features/dimensions/editors/URLPickerTab.tsx @@ -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) => { - {mediaType === MediaType.Icon && } - {mediaType === MediaType.Image && newValue && } + {mediaType === MediaType.Icon && } + {mediaType === MediaType.Image && newValue && ( + + )} {shortName} diff --git a/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx b/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx index b403b1fb805..45dab46482d 100644 --- a/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx +++ b/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx @@ -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; @@ -32,7 +32,7 @@ export function MarkersLegend(props: MarkersLegendProps) { return ( - { 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]; diff --git a/yarn.lock b/yarn.lock index 8d61b583b98..bb0f623ecc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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