diff --git a/packages/manager/.changeset/pr-10378-tech-stories-1713297219037.md b/packages/manager/.changeset/pr-10378-tech-stories-1713297219037.md new file mode 100644 index 00000000000..2e0680fb147 --- /dev/null +++ b/packages/manager/.changeset/pr-10378-tech-stories-1713297219037.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Replace sanitize-html with dompurify ([#10378](https://github.com/linode/manager/pull/10378)) diff --git a/packages/manager/package.json b/packages/manager/package.json index d4fa67a2e41..729dee4b118 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -33,6 +33,7 @@ "chart.js": "~2.9.4", "copy-to-clipboard": "^3.0.8", "country-region-data": "^1.4.5", + "dompurify": "^3.1.0", "flag-icons": "^6.6.5", "font-logos": "^0.18.0", "formik": "~2.1.3", @@ -71,7 +72,6 @@ "redux": "^4.0.4", "redux-thunk": "^2.3.0", "reselect": "^4.0.0", - "sanitize-html": "^2.12.1", "search-string": "^3.1.0", "throttle-debounce": "^2.0.0", "tss-react": "^4.8.2", @@ -133,6 +133,7 @@ "@types/chai-string": "^1.4.5", "@types/chart.js": "^2.9.21", "@types/css-mediaquery": "^0.1.1", + "@types/dompurify": "^3.0.5", "@types/he": "^1.1.0", "@types/highlight.js": "~10.1.0", "@types/jsdom": "^21.1.4", @@ -154,7 +155,6 @@ "@types/react-select": "^3.0.11", "@types/recompose": "^0.30.0", "@types/redux-mock-store": "^1.0.1", - "@types/sanitize-html": "^2.9.1", "@types/throttle-debounce": "^1.0.0", "@types/uuid": "^3.4.3", "@types/yup": "^0.29.13", diff --git a/packages/manager/src/components/HighlightedMarkdown/HighlightedMarkdown.tsx b/packages/manager/src/components/HighlightedMarkdown/HighlightedMarkdown.tsx index 42b7fa6a23b..8ebd0dd9c49 100644 --- a/packages/manager/src/components/HighlightedMarkdown/HighlightedMarkdown.tsx +++ b/packages/manager/src/components/HighlightedMarkdown/HighlightedMarkdown.tsx @@ -15,7 +15,7 @@ import { unsafe_MarkdownIt } from 'src/utilities/markdown'; import { sanitizeHTML } from 'src/utilities/sanitizeHTML'; import { useColorMode } from 'src/utilities/theme'; -import type { IOptions } from 'sanitize-html'; +import type { SanitizeOptions } from 'src/utilities/sanitizeHTML'; hljs.registerLanguage('apache', apache); hljs.registerLanguage('bash', bash); @@ -35,7 +35,7 @@ export type SupportedLanguage = export interface HighlightedMarkdownProps { className?: string; language?: SupportedLanguage; - sanitizeOptions?: IOptions; + sanitizeOptions?: SanitizeOptions; textOrMarkdown: string; } @@ -88,8 +88,7 @@ export const HighlightedMarkdown = (props: HighlightedMarkdownProps) => { const unsafe_parsedMarkdown = unsafe_MarkdownIt.render(textOrMarkdown); const sanitizedHtml = sanitizeHTML({ - // eslint-disable-next-line xss/no-mixed-html - options: sanitizeOptions, + sanitizeOptions, sanitizingTier: 'flexible', text: unsafe_parsedMarkdown, }); diff --git a/packages/manager/src/constants.ts b/packages/manager/src/constants.ts index fd0592be7a6..7fc18183fd3 100644 --- a/packages/manager/src/constants.ts +++ b/packages/manager/src/constants.ts @@ -167,7 +167,14 @@ export const allowedHTMLTagsFlexible: string[] = [ 'tr', ]; -export const allowedHTMLAttr = ['href', 'lang', 'title', 'align']; +export const allowedHTMLAttr = [ + 'href', + 'lang', + 'title', + 'align', + 'class', + 'rel', +]; /** * MBps rate for intra DC migrations (AKA Mutations) diff --git a/packages/manager/src/features/Events/EventRow.tsx b/packages/manager/src/features/Events/EventRow.tsx index fe0998122e3..804af2d74c1 100644 --- a/packages/manager/src/features/Events/EventRow.tsx +++ b/packages/manager/src/features/Events/EventRow.tsx @@ -81,7 +81,7 @@ export const Row = (props: RowProps) => { { }; const errorNotice = () => { - let errorMsg = sanitize(localError || '', { - allowedAttributes: {}, - allowedTags: [], // Disallow all HTML tags, - }); + let errorMsg = sanitizeHTML({ + sanitizeOptions: { + ALLOWED_ATTR: [], + ALLOWED_TAGS: [], // Disallow all HTML tags, + }, + sanitizingTier: 'strict', + text: localError || '', + }).toString(); // match something like: Linode (ID ) const linode = /Linode (.+?) \(ID ([^\)]+)\)/i.exec(errorMsg); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx index 17061da3771..37e3993a09f 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddNodebalancerDrawer.tsx @@ -1,10 +1,9 @@ import { NodeBalancer } from '@linode/api-v4'; import { useTheme } from '@mui/material'; +import { useQueryClient } from '@tanstack/react-query'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import { useQueryClient } from '@tanstack/react-query'; import { useParams } from 'react-router-dom'; -import sanitize from 'sanitize-html'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; @@ -22,6 +21,7 @@ import { queryKey } from 'src/queries/nodebalancers'; import { useGrants, useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { getEntityIdsByPermission } from 'src/utilities/grants'; +import { sanitizeHTML } from 'src/utilities/sanitizeHTML'; interface Props { helperText: string; @@ -103,10 +103,14 @@ export const AddNodebalancerDrawer = (props: Props) => { }; const errorNotice = () => { - let errorMsg = sanitize(localError || '', { - allowedAttributes: {}, - allowedTags: [], // Disallow all HTML tags, - }); + let errorMsg = sanitizeHTML({ + sanitizeOptions: { + ALLOWED_ATTR: [], + ALLOWED_TAGS: [], // Disallow all HTML tags, + }, + sanitizingTier: 'strict', + text: localError || '', + }).toString(); // match something like: NodeBalancer (ID ) const nodebalancer = /NodeBalancer (.+?) \(ID ([^\)]+)\)/i.exec(errorMsg); diff --git a/packages/manager/src/features/NotificationCenter/NotificationData/RenderEvent.tsx b/packages/manager/src/features/NotificationCenter/NotificationData/RenderEvent.tsx index de564901c07..90eef3a557e 100644 --- a/packages/manager/src/features/NotificationCenter/NotificationData/RenderEvent.tsx +++ b/packages/manager/src/features/NotificationCenter/NotificationData/RenderEvent.tsx @@ -35,7 +35,7 @@ export const RenderEvent = React.memo((props: RenderEventProps) => {
- sanitize(text, { - allowedAttributes: { - '*': allowedHTMLAttr, - // "target" and "rel" are allowed because they are handled in the - // transformTags map below. - a: [...allowedHTMLAttr, 'target', 'rel'], - }, - allowedClasses: { - span: ['version'], - }, - allowedTags: getAllowedHTMLTags(sanitizingTier, allowMoreTags), - disallowedTagsMode, - transformTags: { - // This transformation function does the following to anchor tags: - // 1. Turns the into a if the "href" is invalid - // 2. Adds `rel="noopener noreferrer" if _target is "blank" (for security) - // 3. Removes "target" attribute if it's anything other than "_blank" - // 4. Removes custom "rel" attributes +}: SanitizeHTMLOptions) => { + DOMPurify.setConfig({ + ALLOWED_ATTR: allowedHTMLAttr, + ALLOWED_TAGS: getAllowedHTMLTags(sanitizingTier, allowMoreTags), + KEEP_CONTENT: disallowedTagsMode === 'discard' ? false : true, + RETURN_DOM: false, + RETURN_DOM_FRAGMENT: false, + RETURN_TRUSTED_TYPE: false, + ...options, + }); - a: (tagName, attribs) => { - // If the URL is invalid, transform to a span. - const href = attribs.href ?? ''; - if (href && !isURLValid(href)) { - return { - attribs: {}, - tagName: 'span', - }; - } + // Define transform function for anchor tags + const anchorHandler = (node: HTMLAnchorElement) => { + const href = node.getAttribute('href') ?? ''; - // If this link opens a new tab, add "noopener noreferrer" for security. - const target = attribs.target ?? ''; - if (target && target === '_blank') { - return { - attribs: { - ...attribs, - rel: 'noopener noreferrer', - }, - tagName, - }; - } + // If the URL is invalid, transform to a span. + if (href && !isURLValid(href)) { + const span = document.createElement('span'); + span.setAttribute('class', node.getAttribute('class') || ''); - // Otherwise we don't want to allow the "rel" or "target" attributes. - delete attribs.rel; - delete attribs.target; + if (node.parentNode) { + node.parentNode.replaceChild(span, node); + } + } else { + // If this link opens a new tab, add "noopener noreferrer" for security. + const target = node.getAttribute('target') || ''; + if (target === '_blank') { + node.setAttribute('rel', 'noopener noreferrer'); + } else { + node.removeAttribute('rel'); + node.removeAttribute('target'); + } + } + }; - return { - attribs, - tagName, - }; - }, - }, - ...options, - }).trim(); + // Register hooks for DOMPurify + DOMPurify.addHook('uponSanitizeElement', (node, data) => { + if (data.tagName === 'a') { + anchorHandler(node as HTMLAnchorElement); + } else if (data.tagName === 'span') { + // Allow class attribute only for span elements + const classAttr = node.getAttribute('class'); + if (classAttr && classAttr.trim() !== 'version') { + node.removeAttribute('class'); + } + } + }); + + // Perform sanitization + const output = DOMPurify.sanitize(text); + return output.trim(); +}; diff --git a/yarn.lock b/yarn.lock index 30739364956..e1224d5319d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3389,6 +3389,13 @@ resolved "https://registry.yarnpkg.com/@types/doctrine/-/doctrine-0.0.9.tgz#d86a5f452a15e3e3113b99e39616a9baa0f9863f" integrity sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA== +"@types/dompurify@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7" + integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg== + dependencies: + "@types/trusted-types" "*" + "@types/ejs@^3.1.1": version "3.1.5" resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.5.tgz#49d738257cc73bafe45c13cb8ff240683b4d5117" @@ -3783,13 +3790,6 @@ resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.6.tgz#e6e60dad29c2c8c206c026e6dd8d6d1bdda850b8" integrity sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ== -"@types/sanitize-html@^2.9.1": - version "2.11.0" - resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.11.0.tgz#582d8c72215c0228e3af2be136e40e0b531addf2" - integrity sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ== - dependencies: - htmlparser2 "^8.0.0" - "@types/scheduler@*": version "0.16.8" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" @@ -3842,6 +3842,11 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== +"@types/trusted-types@*": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + "@types/unist@*", "@types/unist@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" @@ -5849,11 +5854,6 @@ deepmerge@^2.1.1: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== -deepmerge@^4.2.2: - version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== - default-browser-id@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-3.0.0.tgz#bee7bbbef1f4e75d31f98f4d3f1556a14cea790c" @@ -6008,20 +6008,6 @@ dom-helpers@^5.0.1: "@babel/runtime" "^7.8.7" csstype "^3.0.2" -dom-serializer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" - integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.2" - entities "^4.2.0" - -domelementtype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - domexception@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" @@ -6029,26 +6015,15 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" -domhandler@^5.0.2, domhandler@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" - integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== - dependencies: - domelementtype "^2.3.0" - dompurify@^2.2.0: version "2.4.7" resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.7.tgz#277adeb40a2c84be2d42a8bcd45f582bfa4d0cfc" integrity sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ== -domutils@^3.0.1: +dompurify@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" - integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== - dependencies: - dom-serializer "^2.0.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.0.tgz#8c6b9fe986969a33aa4686bd829cbe8e14dd9445" + integrity sha512-yoU4rhgPKCo+p5UrWWWNKiIq+ToGqmVVhk0PmMYBK4kRsR3/qhemNFL8f6CFmBd4gMwm3F4T7HBoydP5uY07fA== dot-case@^3.0.4: version "3.0.4" @@ -6143,7 +6118,7 @@ enquirer@^2.3.6: ansi-colors "^4.1.1" strip-ansi "^6.0.1" -entities@^4.2.0, entities@^4.4.0: +entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -7716,16 +7691,6 @@ html2canvas@^1.0.0-rc.5: css-line-break "^2.1.0" text-segmentation "^1.0.3" -htmlparser2@^8.0.0: - version "8.0.2" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" - integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - entities "^4.4.0" - http-errors@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" @@ -8170,7 +8135,7 @@ is-plain-obj@^4.0.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== -is-plain-object@5.0.0, is-plain-object@^5.0.0: +is-plain-object@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== @@ -10141,11 +10106,6 @@ parse-json@^5.0.0, parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse-srcset@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" - integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== - parse5@^7.0.0, parse5@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" @@ -10381,7 +10341,7 @@ postcss-load-config@^4.0.1: lilconfig "^3.0.0" yaml "^2.3.4" -postcss@^8.3.11, postcss@^8.4.35: +postcss@^8.4.35: version "8.4.35" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA== @@ -11469,18 +11429,6 @@ safe-regex-test@^1.0.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sanitize-html@^2.12.1: - version "2.12.1" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.12.1.tgz#280a0f5c37305222921f6f9d605be1f6558914c7" - integrity sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA== - dependencies: - deepmerge "^4.2.2" - escape-string-regexp "^4.0.0" - htmlparser2 "^8.0.0" - is-plain-object "^5.0.0" - parse-srcset "^1.0.2" - postcss "^8.3.11" - sax@>=0.6.0: version "1.3.0" resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0"