From 0c337a19420e6bd9715b4c87ab994b23c9f0ea01 Mon Sep 17 00:00:00 2001 From: Bowl42 Date: Wed, 4 Mar 2026 23:30:43 +0800 Subject: [PATCH] fix: sanitize mail content in reply/forward to prevent XSS (#857) * fix: sanitize mail content in reply/forward to prevent XSS - Add DOMPurify to sanitize HTML email content (whitelist-based) - Add escapeHtml for plain text content (escape &<>"') - Guard mail.originalSource with fallback to empty string - Add jsdom for vitest DOM environment (DOMPurify requires DOM) - Add XSS regression tests (script tags, event handlers, HTML escape) - Add contentType assertion for empty message fallback case Co-Authored-By: Claude Opus 4.6 * test: add XSS sanitization E2E screenshots Co-Authored-By: Claude Opus 4.6 * chore: remove temporary screenshots from tree Co-Authored-By: Claude Opus 4.6 * fix: normalize escapeHtml input and add forward text escape test - escapeHtml: convert input via String(str ?? '') to handle non-string values - Add test for plain text forward with special chars (<, &, >) Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- CHANGELOG.md | 1 + CHANGELOG_EN.md | 1 + frontend/package.json | 4 +- .../src/utils/__tests__/mail-actions.test.js | 87 +++++++++++++++++++ frontend/src/utils/mail-actions.js | 38 ++++++-- 5 files changed, 125 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac659a04..58b492b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - fix: |前端| 修复暗色主题下邮件内容文字看不清的问题,优化纯文本邮件和 Shadow DOM 渲染的暗色模式样式 - docs: |文档| 新增 Admin 删除邮件、删除邮箱地址、清空收件箱、清空发件箱 API 文档 - fix: |前端| 修复回复 HTML 格式邮件时丢失原邮件 HTML 内容的问题,优先使用 HTML 原文而非纯文本 +- fix: |安全| 修复回复/转发邮件时的 XSS 风险,使用 DOMPurify 对 HTML 内容进行白名单消毒,对纯文本内容进行 HTML 转义 ### Improvements diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index c1b2f822..6f523f95 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -21,6 +21,7 @@ - fix: |Frontend| Fix email content text being unreadable in dark theme, improve dark mode styles for plain text mail and Shadow DOM rendering - docs: |Docs| Add Admin API documentation for delete mail, delete address, clear inbox, and clear sent items - fix: |Frontend| Fix reply to HTML email losing original HTML content, prefer HTML message over plain text +- fix: |Security| Fix XSS vulnerability in reply/forward mail content, sanitize HTML with DOMPurify whitelist and escape plain text ### Improvements diff --git a/frontend/package.json b/frontend/package.json index a0a15a12..1781c011 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,7 @@ "@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "^5.1.12", "axios": "^1.13.5", + "dompurify": "^3.3.1", "jszip": "^3.10.1", "mail-parser-wasm": "^0.2.1", "naive-ui": "^2.43.2", @@ -43,15 +44,16 @@ "@vicons/fa": "^0.13.0", "@vicons/material": "^0.13.0", "@vitejs/plugin-vue": "^6.0.4", + "jsdom": "^28.1.0", "unplugin-auto-import": "^20.3.0", "unplugin-vue-components": "^30.0.0", "vite": "^7.3.1", "vite-plugin-pwa": "^1.2.0", "vite-plugin-top-level-await": "^1.6.0", "vite-plugin-wasm": "^3.5.0", + "vitest": "^3.1.0", "workbox-build": "^7.4.0", "workbox-window": "^7.4.0", - "vitest": "^3.1.0", "wrangler": "^4.68.1" }, "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" diff --git a/frontend/src/utils/__tests__/mail-actions.test.js b/frontend/src/utils/__tests__/mail-actions.test.js index 215fe2c7..d816b747 100644 --- a/frontend/src/utils/__tests__/mail-actions.test.js +++ b/frontend/src/utils/__tests__/mail-actions.test.js @@ -1,3 +1,4 @@ +// @vitest-environment jsdom import { describe, it, expect } from 'vitest' import { buildReplyModel, buildForwardModel } from '../mail-actions' @@ -29,6 +30,7 @@ describe('buildReplyModel', () => { expect(result.content).toBe( '


Plain text fallback


' ) + expect(result.contentType).toBe('rich') }) it('falls back to plain text when message is null', () => { @@ -95,6 +97,18 @@ describe('buildReplyModel', () => { expect(result.toMail).toBe('plain@example.com') }) + it('defaults toMail to empty string when originalSource is null', () => { + const mail = { + source: 'plain@example.com', + originalSource: null, + subject: 'Test', + message: '', + text: 'body', + } + const result = buildReplyModel(mail, 'Reply') + expect(result.toMail).toBe('') + }) + it('formats subject with reply label', () => { const mail = { source: 'test@example.com', @@ -130,6 +144,46 @@ describe('buildReplyModel', () => { const result = buildReplyModel(mail, 'Reply') expect(result.contentType).toBe('rich') }) + + it('strips script tags from HTML reply content (XSS)', () => { + const mail = { + source: 'attacker@example.com', + originalSource: 'attacker@example.com', + subject: 'XSS', + message: '

Hello

World

', + text: '', + } + const result = buildReplyModel(mail, 'Reply') + expect(result.content).not.toContain('', + text: '', + } + const result = buildForwardModel(mail, 'Forward') + expect(result.content).not.toContain('