From f19b721b3fb11317e0d9974805ea31a165b3da7e Mon Sep 17 00:00:00 2001 From: Andy McHugh Date: Fri, 16 Feb 2024 18:05:45 +0000 Subject: [PATCH] add dompurify Fix grafana review comments: - sanitize the externally provided xml using dompurify - correct typo in timeSlider description This also corrects a bug with all drivable cells being treated as if they had valid links. Now the cursor only changes when there's a configured link. --- package-lock.json | 32 +++++++++++++++++++++++++++----- package.json | 2 ++ src/components/FlowPanel.tsx | 21 ++++++++++++++++++++- src/components/SvgUpdater.tsx | 14 ++++++++------ src/module.ts | 2 +- 5 files changed, 58 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0a13be5..b73badb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "flow", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "flow", - "version": "1.0.0", + "version": "1.0.1", "license": "Apache-2.0", "dependencies": { "@emotion/css": "11.10.6", @@ -14,6 +14,7 @@ "@grafana/runtime": "10.0.3", "@grafana/schema": "10.0.3", "@grafana/ui": "10.0.3", + "dompurify": "^3.0.8", "js-yaml-loader": "^1.2.2", "react": "18.2.0", "react-dom": "18.2.0", @@ -30,6 +31,7 @@ "@swc/jest": "^0.2.26", "@testing-library/jest-dom": "6.1.4", "@testing-library/react": "14.0.0", + "@types/dompurify": "^3.0.5", "@types/jest": "^29.5.0", "@types/lodash": "^4.14.194", "@types/node": "^20.8.7", @@ -2177,6 +2179,11 @@ "react-dom": "^17.0.0 || ^18.0.0" } }, + "node_modules/@grafana/data/node_modules/dompurify": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz", + "integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==" + }, "node_modules/@grafana/data/node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -5377,6 +5384,15 @@ "@types/d3-color": "*" } }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", @@ -5621,6 +5637,12 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -8903,9 +8925,9 @@ } }, "node_modules/dompurify": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz", - "integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==" + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.8.tgz", + "integrity": "sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ==" }, "node_modules/earcut": { "version": "2.2.4", diff --git a/package.json b/package.json index a020c76..2b95446 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@swc/jest": "^0.2.26", "@testing-library/jest-dom": "6.1.4", "@testing-library/react": "14.0.0", + "@types/dompurify": "^3.0.5", "@types/jest": "^29.5.0", "@types/lodash": "^4.14.194", "@types/node": "^20.8.7", @@ -63,6 +64,7 @@ "@grafana/runtime": "10.0.3", "@grafana/schema": "10.0.3", "@grafana/ui": "10.0.3", + "dompurify": "^3.0.8", "js-yaml-loader": "^1.2.2", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/src/components/FlowPanel.tsx b/src/components/FlowPanel.tsx index f8e9405..4e5ff09 100644 --- a/src/components/FlowPanel.tsx +++ b/src/components/FlowPanel.tsx @@ -11,9 +11,24 @@ import { seriesExtend, seriesInterpolate , seriesTransform } from 'components/Ti import { TimeSliderFactory } from 'components/TimeSlider'; import { displayColorsInner, displayDataInner, displayMappingsInner } from 'components/TroubleshootingEditor'; import { primeColorCache, appendUrlParams } from 'components/Utils'; +import { addHook, sanitize } from 'dompurify'; interface Props extends PanelProps {} +// Sanitize externally defined SVG using DOMPurify. DrawIO svg's rely on +// foreignObject. In addition, the cursor changing to a pointer on mouse-over of +// cells with links, relies on foreignObject defining 'pointer-events: none'. As +// such sanitization is configured to retain these objects and put back the +// pointer-events attribute. +addHook('afterSanitizeAttributes', function(el) { + if (el.nodeName === 'foreignObject') { + el.setAttribute('pointer-events', 'none'); + } +}); +function sanitizeSvgStr(svgStr: string) { + return sanitize(svgStr, {ADD_TAGS: ['foreignObject']}); +} + const getStyles = () => { return { wrapper: css` @@ -92,7 +107,8 @@ export const FlowPanel: React.FC = ({ options, data, width, height, timeZ useEffect(() => { if (svgStr && panelConfig && siteConfig) { configInit(siteConfig, panelConfig); - const svgDoc = new DOMParser().parseFromString(svgStr, "text/xml"); + + const svgDoc = new DOMParser().parseFromString(sanitizeSvgStr(svgStr), "text/xml"); const svgAttribs = svgInit(svgDoc, panelConfig, siteConfig); primeColorCache(grafanaTheme.current, svgAttribs); svgHolderRef.current = { @@ -224,6 +240,9 @@ export const FlowPanel: React.FC = ({ options, data, width, height, timeZ ` )} onClick={clickHandlerRef.current} + // The externally received svg is sanitised when read in via sanitizeSvgStr which uses + // dompurify. We don't re-sanitize it on each rendering as we are in control of the + // modifications being made. dangerouslySetInnerHTML={{__html: svgElement.outerHTML}}/>
{timeSliderEnabled && timeSlider}
diff --git a/src/components/SvgUpdater.tsx b/src/components/SvgUpdater.tsx index 11d6f4e..071037d 100644 --- a/src/components/SvgUpdater.tsx +++ b/src/components/SvgUpdater.tsx @@ -74,8 +74,12 @@ function dimensionCoherence(doc: Document) { function recurseElements(el: HTMLElement, cellData: SvgCell, cellIdMaker: CellIdMaker): boolean { const setAttributes = function(el: HTMLElement) { el.style.whiteSpace = 'pre'; - el.style.cursor = 'pointer'; - el.setAttribute('cursor', 'pointer'); + + if (cellData.cellProps.link) { + el.style.cursor = 'pointer'; + el.setAttribute('cursor', 'pointer'); + } + if (!el.id) { el.setAttribute('id', cellIdMaker()); } @@ -130,12 +134,10 @@ export function svgInit(doc: Document, panelConfig: PanelConfig, siteConfig: Sit const link = panelConfigCell ? panelConfigCell.link : null; if (link) { cell.textElements.forEach((el) => { - let cellId = el.id; - elementLinks.set(cellId, link); + elementLinks.set(el.id, link); }); cell.fillElements.forEach((el) => { - let cellId = el.id; - elementLinks.set(cellId, link); + elementLinks.set(el.id, link); }); } }); diff --git a/src/module.ts b/src/module.ts index 4cd45e7..dccf2ae 100644 --- a/src/module.ts +++ b/src/module.ts @@ -43,7 +43,7 @@ export const plugin = new PanelPlugin(FlowPanel).setPanelOptions((b path: 'timeSliderEnabled', name: 'Time Slider', description: `When enabled a time-slider is added to the bottom of - the panel to support visualisaion of any time point in the time range.`, + the panel to support visualization of any time point in the time range.`, defaultValue: true, }) .addBooleanSwitch({