From c022534e3848a5d45c0b3face23b43aa44e4400a Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Thu, 22 Dec 2022 16:45:48 -0700 Subject: [PATCH] [v9.2.x] SVG: Add dompurify preprocessor step (#704) * SVG: Add dompurify preprocessor step (#698) * add sanitized SVG component (cherry picked from commit dbbe819368f507b9493b7a3cf6e080a5dd752b3e) * remove added file Co-authored-by: Ryan McKinley --- package.json | 2 ++ .../grafana-ui/src/components/Icon/Icon.tsx | 6 ++++- .../app/core/components/SVG/SanitizedSVG.tsx | 18 +++++++++++++ public/app/features/canvas/elements/icon.tsx | 4 +-- .../dimensions/editors/FileUploader.tsx | 7 +++--- .../dimensions/editors/ResourceCards.tsx | 4 +-- .../dimensions/editors/ResourcePicker.tsx | 6 ++--- .../dimensions/editors/URLPickerTab.tsx | 4 +-- public/app/features/storage/FileView.tsx | 4 +-- .../panel/geomap/components/MarkersLegend.tsx | 4 +-- .../app/plugins/panel/geomap/style/markers.ts | 3 +++ yarn.lock | 25 +++++++++++++++++++ 12 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 public/app/core/components/SVG/SanitizedSVG.tsx diff --git a/package.json b/package.json index bd94165aa4b..e26f95d855a 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,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.12", "@types/enzyme-adapter-react-16": "1.0.6", "@types/eslint": "8.4.5", @@ -315,6 +316,7 @@ "dangerously-set-html-content": "1.0.9", "date-fns": "2.29.1", "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/packages/grafana-ui/src/components/Icon/Icon.tsx b/packages/grafana-ui/src/components/Icon/Icon.tsx index c601c17adc1..a411c06a6a1 100644 --- a/packages/grafana-ui/src/components/Icon/Icon.tsx +++ b/packages/grafana-ui/src/components/Icon/Icon.tsx @@ -57,9 +57,13 @@ export const Icon = React.forwardRef( console.warn('Icon component passed an invalid icon name', name); } + if (!name || name.includes('..')) { + return
invalid icon name
; + } + const svgSize = getSvgSize(size); const svgHgt = svgSize; - const svgWid = name?.startsWith('gf-bar-align') ? 16 : name?.startsWith('gf-interp') ? 30 : svgSize; + const svgWid = name.startsWith('gf-bar-align') ? 16 : name.startsWith('gf-interp') ? 30 : svgSize; const subDir = getIconSubDir(name, type); const svgPath = `${iconRoot}${subDir}/${name}.svg`; 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 cb4920e8948..8d7c8a4c876 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, @@ -59,7 +59,7 @@ export function IconDisplay(props: CanvasElementProps) { }; return ( - >; mediaType: MediaType; @@ -36,8 +37,8 @@ export const FileUploader = ({ mediaType, setFormData, setUpload, error }: Props const Preview = () => (
- {mediaType === MediaType.Icon && } - {mediaType === MediaType.Image && } + {mediaType === MediaType.Icon && } + {mediaType === MediaType.Image && Preview of the uploaded file}
); diff --git a/public/app/features/dimensions/editors/ResourceCards.tsx b/public/app/features/dimensions/editors/ResourceCards.tsx index afd7e0d6bd1..612c78eed77 100644 --- a/public/app/features/dimensions/editors/ResourceCards.tsx +++ b/public/app/features/dimensions/editors/ResourceCards.tsx @@ -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') ? ( - + ) : ( )} diff --git a/public/app/features/dimensions/editors/ResourcePicker.tsx b/public/app/features/dimensions/editors/ResourcePicker.tsx index 30246d6aa0d..ecfb63f3ef4 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={