From 6ba85a1856e631ff0acb57c011976cdc02fdc155 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 8 Aug 2026 02:59:43 +0100 Subject: [PATCH] fix(frontend): update vulnerable sanitizer dependencies --- frontend-modern/package-lock.json | 46 ++++++------- frontend-modern/package.json | 2 +- .../AI/__tests__/aiChatUtils.test.ts | 66 +++++++++++++++++++ .../docs/__tests__/docMarkdown.test.ts | 21 ++++++ 4 files changed, 111 insertions(+), 24 deletions(-) diff --git a/frontend-modern/package-lock.json b/frontend-modern/package-lock.json index a65cc77058..f0aad16410 100644 --- a/frontend-modern/package-lock.json +++ b/frontend-modern/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@solidjs/router": "^0.10.10", - "dompurify": "^3.4.12", + "dompurify": "^3.4.13", "highlight.js": "^11.11.1", "lucide-solid": "^0.545.0", "marked": "^17.0.1", @@ -1029,9 +1029,9 @@ "license": "MIT" }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -1110,9 +1110,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -2810,16 +2810,16 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/braces": { @@ -3361,9 +3361,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.12", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", - "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -3684,9 +3684,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -4201,9 +4201,9 @@ "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -5225,9 +5225,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { diff --git a/frontend-modern/package.json b/frontend-modern/package.json index 91266241de..7d4f330823 100644 --- a/frontend-modern/package.json +++ b/frontend-modern/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "@solidjs/router": "^0.10.10", - "dompurify": "^3.4.12", + "dompurify": "^3.4.13", "highlight.js": "^11.11.1", "lucide-solid": "^0.545.0", "marked": "^17.0.1", diff --git a/frontend-modern/src/components/AI/__tests__/aiChatUtils.test.ts b/frontend-modern/src/components/AI/__tests__/aiChatUtils.test.ts index 072f8892d3..c35073a3c8 100644 --- a/frontend-modern/src/components/AI/__tests__/aiChatUtils.test.ts +++ b/frontend-modern/src/components/AI/__tests__/aiChatUtils.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import * as utils from '@/components/AI/aiChatUtils'; +import DOMPurify from 'dompurify'; import { marked } from 'marked'; import type { ModelInfo } from '@/types/ai'; @@ -175,6 +176,71 @@ describe('aiChatUtils', () => { expect(output).toContain('target="_blank"'); expect(output).toContain('rel="noopener noreferrer"'); }); + + it('removes executable descendants from an advisory-shaped detached-subtree payload', () => { + const output = utils.renderMarkdown( + [ + '', + 'safe link', + ].join('\n'), + ); + const template = document.createElement('template'); + template.innerHTML = output; + + expect(template.content.querySelector('footer, img, [onload], [onerror], script')).toBeNull(); + const link = template.content.querySelector('a'); + expect(link?.getAttribute('href')).toBe('https://example.com'); + expect(link?.getAttribute('target')).toBe('_blank'); + expect(link?.getAttribute('rel')).toBe('noopener noreferrer'); + }); + + it.each(['beforeSanitizeElements', 'uponSanitizeElement'] as const)( + 'neutralizes a hook-detached IN_PLACE subtree through the %s hook', + (hookName) => { + // Configure the same singleton hooks used by renderMarkdown before + // exercising DOMPurify's patched IN_PLACE hook-detachment path. + utils.renderMarkdown('```ts\nconst safe = true;\n```'); + + const root = document.createElement('div'); + root.innerHTML = + '
safe
'; + const detachedTail = root.querySelector('#detached-tail'); + expect(detachedTail).not.toBeNull(); + + const removeSection = (node: Node) => { + if (node.nodeName === 'SECTION') (node as Element).remove(); + }; + if (hookName === 'beforeSanitizeElements') { + DOMPurify.addHook('beforeSanitizeElements', removeSection); + } else { + DOMPurify.addHook('uponSanitizeElement', removeSection); + } + + try { + const result = DOMPurify.sanitize(root, { + ALLOWED_TAGS: ['div', 'section', '#text'], + IN_PLACE: true, + }); + + expect(result).toBe(root); + expect(root.innerHTML).toBe('
safe
'); + expect(detachedTail?.isConnected).toBe(false); + expect(detachedTail?.getAttribute('onerror')).toBeNull(); + + const probe = document.createElement('div'); + probe.innerHTML = `${root.outerHTML}${detachedTail?.outerHTML ?? ''}`; + expect(probe.querySelector('[onerror], [onload], script')).toBeNull(); + } finally { + if (hookName === 'beforeSanitizeElements') { + DOMPurify.removeHook('beforeSanitizeElements', removeSection); + } else { + DOMPurify.removeHook('uponSanitizeElement', removeSection); + } + } + }, + ); }); describe('Mention ID format contracts', () => { diff --git a/frontend-modern/src/features/docs/__tests__/docMarkdown.test.ts b/frontend-modern/src/features/docs/__tests__/docMarkdown.test.ts index e27d6b476f..bce5221d61 100644 --- a/frontend-modern/src/features/docs/__tests__/docMarkdown.test.ts +++ b/frontend-modern/src/features/docs/__tests__/docMarkdown.test.ts @@ -108,6 +108,27 @@ describe('renderDocMarkdown', () => { expect(html).not.toContain('onerror'); expect(html).not.toContain('javascript:'); }); + + it('neutralizes an advisory-shaped nested resource payload before link rewriting', () => { + const html = renderDocMarkdown( + [ + '
', + ' ', + '
', + '
safe link
', + ].join('\n'), + 'README', + ); + const template = document.createElement('template'); + template.innerHTML = html; + + expect(template.content.querySelector('footer, [onload], [onerror], script')).toBeNull(); + const image = template.content.querySelector('img'); + expect(image?.getAttribute('src')).toBe('x'); + const link = template.content.querySelector('a'); + expect(link?.getAttribute('href')).toBe('/docs/INSTALL'); + expect(link?.hasAttribute('data-doc-link')).toBe(true); + }); }); describe('wrapTables', () => {