diff --git a/package.json b/package.json index a9079fa8025..c9255b114bd 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "@types/d3-force": "^2.1.0", "@types/d3-scale-chromatic": "1.3.1", "@types/debounce-promise": "3.1.5", + "@types/dompurify": "^2", "@types/eslint": "8.4.9", "@types/file-saver": "2.0.5", "@types/glob": "^8.0.0", @@ -316,6 +317,7 @@ "dangerously-set-html-content": "1.0.9", "date-fns": "2.29.3", "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,7 +37,7 @@ export const FileUploader = ({ mediaType, setFormData, setUpload, error }: Props const Preview = () => (
- {mediaType === MediaType.Icon && } + {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 49aa6ea804c..57d01604cda 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={