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 =
+ '