From e14a512421f20e1fa51cc63c6a3c4fea58a4290b Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:07:41 +0300 Subject: [PATCH] fix: sanitize plugin svg icons --- package-lock.json | 17 ++++++ package.json | 1 + src/components/plugins/view/PluginIcon.tsx | 63 ++++++++++++---------- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index f1937b10..0ef68958 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "cmdk": "^1.1.1", "cors": "^2.8.5", "cross-spawn": "^7.0.3", + "dompurify": "^3.4.7", "express": "^4.18.2", "fuse.js": "^7.0.0", "gray-matter": "^4.0.3", @@ -4580,6 +4581,13 @@ "@types/node": "*" } }, + "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==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -7485,6 +7493,15 @@ "node": ">=0.10.0" } }, + "node_modules/dompurify": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.7.tgz", + "integrity": "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", diff --git a/package.json b/package.json index fc6b7d75..1cd81aca 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "cmdk": "^1.1.1", "cors": "^2.8.5", "cross-spawn": "^7.0.3", + "dompurify": "^3.4.7", "express": "^4.18.2", "fuse.js": "^7.0.0", "gray-matter": "^4.0.3", diff --git a/src/components/plugins/view/PluginIcon.tsx b/src/components/plugins/view/PluginIcon.tsx index 4999dc10..ce8c90fc 100644 --- a/src/components/plugins/view/PluginIcon.tsx +++ b/src/components/plugins/view/PluginIcon.tsx @@ -1,4 +1,6 @@ import { useState, useEffect } from 'react'; +import DOMPurify from 'dompurify'; + import { authenticatedFetch } from '../../../utils/api'; type Props = { @@ -10,38 +12,43 @@ type Props = { // Module-level cache so repeated renders don't re-fetch const svgCache = new Map(); +const FORBIDDEN_SVG_TAGS = [ + 'script', + 'foreignObject', + 'iframe', + 'object', + 'embed', + 'link', + 'meta', + 'style', + 'animate', + 'set', + 'animateTransform', + 'animateMotion', +]; + +const FORBIDDEN_SVG_ATTRS = [ + 'href', + 'xlink:href', + 'src', + 'style', +]; + function sanitizeSvg(svgText: string): string | null { + const sanitized = DOMPurify.sanitize(svgText, { + USE_PROFILES: { svg: true, svgFilters: true }, + FORBID_TAGS: FORBIDDEN_SVG_TAGS, + FORBID_ATTR: FORBIDDEN_SVG_ATTRS, + }); + + if (!sanitized) return null; + try { - const doc = new DOMParser().parseFromString(svgText, 'image/svg+xml'); + const doc = new DOMParser().parseFromString(sanitized, 'image/svg+xml'); const root = doc.documentElement; if (!root || root.nodeName.toLowerCase() !== 'svg') return null; - - doc - .querySelectorAll('script,foreignObject,iframe,object,embed,link,meta,style') - .forEach((el) => el.remove()); - - const walker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); - const elements: Element[] = [root]; - while (walker.nextNode()) { - elements.push(walker.currentNode as Element); - } - - elements.forEach((el) => { - Array.from(el.attributes).forEach((attr) => { - const name = attr.name.toLowerCase(); - const value = attr.value.trim().toLowerCase(); - if ( - name.startsWith('on') || - name === 'href' || - name === 'xlink:href' || - value.startsWith('javascript:') - ) { - el.removeAttribute(attr.name); - } - }); - }); - - return new XMLSerializer().serializeToString(root); + if (doc.querySelector('parsererror')) return null; + return sanitized; } catch { return null; }