From b40d9eda41629b484277e78aac13753cb61cb8db Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Thu, 28 May 2026 21:28:10 +0200 Subject: [PATCH] fix: use a browser compatible sanitizer santize-html is intended to be used in a node environment. This means it can (and does) import thing like path, fs, and url. While we do provide polyfills for the client, that's not ideal and it forces any other consumer to do the same. --- package.json | 5 +- pnpm-lock.yaml | 84 ++++----------------- src/quiz-question/transcript/transcript.tsx | 10 +-- 3 files changed, 20 insertions(+), 79 deletions(-) diff --git a/package.json b/package.json index 45636d36..4c4b2c65 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,8 @@ "@headlessui/react": "1.7.19", "@radix-ui/react-tabs": "1.1.13", "babel-plugin-prismjs": "2.1.0", - "prismjs": "1.30.0", - "sanitize-html": "2.17.0" + "dompurify": "3.4.7", + "prismjs": "1.30.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", @@ -88,7 +88,6 @@ "@types/prismjs": "1.26.5", "@types/react": "18.0.0", "@types/react-dom": "18.0.0", - "@types/sanitize-html": "2.16.0", "@vitest/eslint-plugin": "1.6.16", "@vitest/ui": "4.0.18", "autoprefixer": "10.4.22", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da238efe..a11e44eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: babel-plugin-prismjs: specifier: 2.1.0 version: 2.1.0(prismjs@1.30.0) + dompurify: + specifier: 3.4.7 + version: 3.4.7 prismjs: specifier: 1.30.0 version: 1.30.0 @@ -35,9 +38,6 @@ importers: react-dom: specifier: ^18.0.0 || ^19.0.0 version: 18.3.1(react@18.3.1) - sanitize-html: - specifier: 2.17.0 - version: 2.17.0 devDependencies: '@babel/core': specifier: 7.28.5 @@ -117,9 +117,6 @@ importers: '@types/react-dom': specifier: 18.0.0 version: 18.0.0 - '@types/sanitize-html': - specifier: 2.16.0 - version: 2.16.0 '@vitest/eslint-plugin': specifier: 1.6.16 version: 1.6.16(@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.5.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.5.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.5.1))(typescript@5.9.3)(vitest@4.0.18) @@ -1963,15 +1960,15 @@ packages: '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} - '@types/sanitize-html@2.16.0': - resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==} - '@types/scheduler@0.26.0': resolution: {integrity: sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==} '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} @@ -2851,9 +2848,6 @@ packages: dom-serializer@1.4.1: resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} @@ -2861,16 +2855,12 @@ packages: resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} engines: {node: '>= 4'} - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} + dompurify@3.4.7: + resolution: {integrity: sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==} domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} @@ -3415,9 +3405,6 @@ packages: htmlparser2@6.1.0: resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} - htmlparser2@8.0.2: - resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -3616,10 +3603,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-plain-object@5.0.0: - resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} - engines: {node: '>=0.10.0'} - is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -4159,9 +4142,6 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - parse-srcset@1.0.2: - resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} - parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} @@ -4758,9 +4738,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sanitize-html@2.17.0: - resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==} - saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -7359,14 +7336,13 @@ snapshots: '@types/resolve@1.20.6': {} - '@types/sanitize-html@2.16.0': - dependencies: - htmlparser2: 8.0.2 - '@types/scheduler@0.26.0': {} '@types/semver@7.5.8': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/uuid@9.0.8': {} '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.5.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.5.1))(typescript@5.9.3)': @@ -8378,21 +8354,15 @@ snapshots: domhandler: 4.3.1 entities: 2.2.0 - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - domelementtype@2.3.0: {} domhandler@4.3.1: dependencies: domelementtype: 2.3.0 - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 + dompurify@3.4.7: + optionalDependencies: + '@types/trusted-types': 2.0.7 domutils@2.8.0: dependencies: @@ -8400,12 +8370,6 @@ snapshots: domelementtype: 2.3.0 domhandler: 4.3.1 - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - dot-case@3.0.4: dependencies: no-case: 3.0.4 @@ -9189,13 +9153,6 @@ snapshots: domutils: 2.8.0 entities: 2.2.0 - htmlparser2@8.0.2: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 4.5.0 - http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -9382,8 +9339,6 @@ snapshots: is-number@7.0.0: {} - is-plain-object@5.0.0: {} - is-potential-custom-element-name@1.0.1: {} is-reference@1.2.1: @@ -9945,8 +9900,6 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parse-srcset@1.0.2: {} - parse5@7.1.2: dependencies: entities: 4.5.0 @@ -10566,15 +10519,6 @@ snapshots: safer-buffer@2.1.2: {} - sanitize-html@2.17.0: - dependencies: - deepmerge: 4.3.1 - escape-string-regexp: 4.0.0 - htmlparser2: 8.0.2 - is-plain-object: 5.0.0 - parse-srcset: 1.0.2 - postcss: 8.5.6 - saxes@6.0.0: dependencies: xmlchars: 2.2.0 diff --git a/src/quiz-question/transcript/transcript.tsx b/src/quiz-question/transcript/transcript.tsx index c500d9e2..a99878ff 100644 --- a/src/quiz-question/transcript/transcript.tsx +++ b/src/quiz-question/transcript/transcript.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useState } from "react"; -import sanitizeHtml from "sanitize-html"; +import DOMPurify from "dompurify"; interface TranscriptProps { /** @@ -23,11 +23,9 @@ export const Transcript = ({ transcript }: TranscriptProps) => { const sanitizedTranscript = useMemo( () => - sanitizeHtml(transcript, { - allowedTags: ["ruby", "rt", "rp", "b", "strong", "i", "em", "p"], - allowedAttributes: { - p: ["class"], - }, + DOMPurify.sanitize(transcript, { + ALLOWED_TAGS: ["ruby", "rt", "rp", "b", "strong", "i", "em", "p"], + ALLOWED_ATTR: ["class"], }), [transcript], );