diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d5d8550d..0eb4c188 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,8 +9,10 @@ "version": "0.0.0", "dependencies": { "@reduxjs/toolkit": "^2.11.2", + "@types/dompurify": "^3.0.5", "@xyflow/react": "^12.8.3", "antd": "^5.27.0", + "dompurify": "^3.4.10", "i18next": "^25.8.0", "jssha": "^3.3.1", "jszip": "^3.10.1", @@ -2416,6 +2418,15 @@ "@types/ms": "*" } }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2544,6 +2555,12 @@ "dev": true, "license": "MIT" }, + "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" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -3988,6 +4005,15 @@ "csstype": "^3.0.2" } }, + "node_modules/dompurify": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.10.tgz", + "integrity": "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/duck": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", diff --git a/frontend/package.json b/frontend/package.json index b3be598e..d66587a1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,8 +12,10 @@ }, "dependencies": { "@reduxjs/toolkit": "^2.11.2", + "@types/dompurify": "^3.0.5", "@xyflow/react": "^12.8.3", "antd": "^5.27.0", + "dompurify": "^3.4.10", "i18next": "^25.8.0", "jssha": "^3.3.1", "jszip": "^3.10.1", diff --git a/frontend/src/components/file-preview/DocxPreview.tsx b/frontend/src/components/file-preview/DocxPreview.tsx index 2249aac5..316d3345 100644 --- a/frontend/src/components/file-preview/DocxPreview.tsx +++ b/frontend/src/components/file-preview/DocxPreview.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import DOMPurify from 'dompurify'; import mammoth from 'mammoth'; export interface DocxPreviewProps { @@ -28,7 +29,15 @@ export const DocxPreview: React.FC = ({ const arrayBuffer = await blob.arrayBuffer(); const result = await mammoth.convertToHtml({ arrayBuffer }); - setHtml(result.value); + // Sanitize HTML to prevent XSS from malicious DOCX files (FCE) + const sanitized = DOMPurify.sanitize(result.value, { + ALLOWED_TAGS: ['h1','h2','h3','h4','h5','h6','p','br','hr','ul','ol','li', + 'table','thead','tbody','tr','th','td','strong','em','b','i','u','s', + 'a','img','sup','sub','pre','code','blockquote','span','div'], + ALLOWED_ATTR: ['href','target','src','alt','width','height','colspan', + 'rowspan','style','class','id','data-*'], + }); + setHtml(sanitized); } catch (err) { setError('Failed to convert Word document'); } finally {