mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SVG: Add dompurify preprocessor step (#62143)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
parent
dd147a3c31
commit
8b574e22b5
@ -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",
|
||||
|
@ -57,9 +57,13 @@ export const Icon = React.forwardRef<HTMLDivElement, IconProps>(
|
||||
console.warn('Icon component passed an invalid icon name', name);
|
||||
}
|
||||
|
||||
if (!name || name.includes('..')) {
|
||||
return <div ref={ref}>invalid icon name</div>;
|
||||
}
|
||||
|
||||
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`;
|
||||
|
||||
|
18
public/app/core/components/SVG/SanitizedSVG.tsx
Normal file
18
public/app/core/components/SVG/SanitizedSVG.tsx
Normal file
@ -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;
|
||||
}
|
@ -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 (
|
||||
<SVG
|
||||
<SanitizedSVG
|
||||
onClick={onClick}
|
||||
src={data.path}
|
||||
style={svgStyle}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { Dispatch, SetStateAction, useState } from 'react';
|
||||
import SVG from 'react-inlinesvg';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { FileDropzone, useStyles2, Button, DropzoneFile, Field } from '@grafana/ui';
|
||||
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
|
||||
|
||||
import { MediaType } from '../types';
|
||||
|
||||
interface Props {
|
||||
setFormData: Dispatch<SetStateAction<FormData>>;
|
||||
mediaType: MediaType;
|
||||
@ -36,7 +37,7 @@ export const FileUploader = ({ mediaType, setFormData, setUpload, error }: Props
|
||||
const Preview = () => (
|
||||
<Field label="Preview">
|
||||
<div className={styles.iconPreview}>
|
||||
{mediaType === MediaType.Icon && <SVG src={file} className={styles.img} />}
|
||||
{mediaType === MediaType.Icon && <SanitizedSVG src={file} className={styles.img} />}
|
||||
{mediaType === MediaType.Image && <img src={file} alt="Preview of the uploaded file" className={styles.img} />}
|
||||
</div>
|
||||
</Field>
|
||||
|
@ -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} alt="" 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,7 +33,7 @@ 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.Icon && <SanitizedSVG src={imgSrc} className={styles.img} />}
|
||||
{mediaType === MediaType.Image && newValue && (
|
||||
<img src={imgSrc} alt="Preview of the selected URL" className={styles.img} />
|
||||
)}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import InlineSVG from 'react-inlinesvg/esm';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||
import { Button, HorizontalGroup, Icon, IconName, useStyles2 } from '@grafana/ui';
|
||||
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
|
||||
|
||||
import { useAppNotification } from '../../../core/copy/appNotification';
|
||||
import { SavedQuery } from '../api/SavedQueriesApi';
|
||||
@ -150,7 +150,7 @@ export const QueryEditorDrawerHeader = ({ savedQuery, onDismiss, onSavedQueryCha
|
||||
<a onClick={implementationComingSoonAlert}>
|
||||
<div>
|
||||
{option.src ? (
|
||||
<InlineSVG src={option.src} className={styles.optionSvg} />
|
||||
<SanitizedSVG src={option.src} className={styles.optionSvg} />
|
||||
) : (
|
||||
<Icon name={option.icon} className={styles.menuIconClassName} />
|
||||
)}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { isString } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import SVG from 'react-inlinesvg';
|
||||
import { useAsync } from 'react-use';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||
import { CodeEditor, useStyles2 } from '@grafana/ui';
|
||||
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
|
||||
|
||||
import { getGrafanaStorage } from './storage';
|
||||
import { StorageView } from './types';
|
||||
@ -55,7 +55,7 @@ export function FileView({ listing, path, onPathChange, view }: Props) {
|
||||
case 'svg':
|
||||
return (
|
||||
<div>
|
||||
<SVG src={src} className={styles.icon} />
|
||||
<SanitizedSVG src={src} className={styles.icon} />
|
||||
</div>
|
||||
);
|
||||
case 'image':
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import React, { useMemo } from 'react';
|
||||
import SVG from 'react-inlinesvg';
|
||||
import { useObservable } from 'react-use';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
@ -9,6 +8,7 @@ import { DataFrame, formattedValueToString, getFieldColorModeForField, GrafanaTh
|
||||
import { getMinMaxAndDelta } from '@grafana/data/src/field/scale';
|
||||
import { useStyles2, VizLegendItem } from '@grafana/ui';
|
||||
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
||||
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
|
||||
import { config } from 'app/core/config';
|
||||
import { DimensionSupplier } from 'app/features/dimensions';
|
||||
import { getThresholdItems } from 'app/plugins/panel/state-timeline/utils';
|
||||
@ -58,7 +58,7 @@ export function MarkersLegend(props: MarkersLegendProps) {
|
||||
<div className={style.infoWrap}>
|
||||
<div className={style.layerName}>{layerName}</div>
|
||||
<div className={cx(style.layerBody, 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';
|
||||
|
||||
@ -247,6 +248,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
yarn.lock
25
yarn.lock
@ -10809,6 +10809,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/eslint-scope@npm:^3.7.3":
|
||||
version: 3.7.4
|
||||
resolution: "@types/eslint-scope@npm:3.7.4"
|
||||
@ -11990,6 +11999,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"
|
||||
@ -18333,6 +18349,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dompurify@npm:^2.4.1":
|
||||
version: 2.4.3
|
||||
resolution: "dompurify@npm:2.4.3"
|
||||
checksum: b440981f2a38cada2085759cc3d1e2f94571afc34343d011a8a6aa1ad91ae6abf651adbfa4994b0e2283f0ce81f7891cdb04b67d0b234c8d190cb70e9691f026
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"domutils@npm:^2.5.2, domutils@npm:^2.6.0":
|
||||
version: 2.8.0
|
||||
resolution: "domutils@npm:2.8.0"
|
||||
@ -21665,6 +21688,7 @@ __metadata:
|
||||
"@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
|
||||
@ -21747,6 +21771,7 @@ __metadata:
|
||||
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
|
||||
esbuild: 0.16.17
|
||||
esbuild-loader: 2.21.0
|
||||
|
Loading…
Reference in New Issue
Block a user