fix(frontend): sanitize announcement HTML (#1039)

Sanitize HTML announcements in both the About page and startup notification through a shared DOMPurify helper. Add regression tests and bilingual changelog entries.

Co-authored-by: tuanaiseo <tuanaiseo@gmail.com>
This commit is contained in:
tuanaiseo
2026-07-28 16:49:32 +07:00
committed by GitHub
parent 4c1e593d07
commit 8c883b269f
6 changed files with 38 additions and 2 deletions

View File

@@ -15,6 +15,7 @@
### Bug Fixes
- fix: |Frontend| 使用共享 DOMPurify 净化逻辑处理关于页面与启动通知中的 HTML 公告,避免 `ANNOUNCEMENT` 中的可执行标签或事件属性造成 XSS
- fix: |Worker| 按邮件认证规范修复垃圾邮件检测SPF、DKIM、DMARC 的 `none` 及 SPF/DKIM `neutral` 按认证方法不存在处理,并忽略未注册结果和不支持的方法版本;`JUNK_MAIL_FORCE_PASS_LIST` 仍要求明确返回受支持的 `pass`
- fix: |Admin| 管理后台删除邮箱地址时,先删除该地址的邮件、发件记录、自动回复等关联数据,最后再删除地址本身;此前地址行先被删除导致按地址名匹配的子查询查不到数据,邮件等记录被遗留在数据库中
- fix: |AI 提取| 强化提示词,要求 AI 保持邮件原始链接域名避免小模型改写验证链接域名导致错误跳转issue #1072

View File

@@ -15,6 +15,7 @@
### Bug Fixes
- fix: |Frontend| Sanitize HTML announcements in both the About page and startup notification through a shared DOMPurify helper, preventing executable tags or event attributes in `ANNOUNCEMENT` from causing XSS
- fix: |Worker| Align junk-mail checking with authentication standards: treat SPF, DKIM, and DMARC `none` plus SPF/DKIM `neutral` as absent, and ignore unregistered results and unsupported method versions; `JUNK_MAIL_FORCE_PASS_LIST` still requires an explicit supported `pass`
- fix: |Admin| When deleting an address from the admin panel, delete its mails, sender records, sendbox and auto-reply entries before removing the address row itself; previously the address row was deleted first, so the name-based subqueries matched nothing and the mails were left orphaned in the database
- fix: |AI Extract| Strengthen the prompt to keep original link domains from the email, preventing small models from rewriting verification-link domains (issue #1072)

View File

@@ -5,6 +5,7 @@ import axios from 'axios'
import i18n from '../i18n'
import { getFingerprint } from '../utils/fingerprint'
import { safeBearerHeader, safeHeaderValue } from '../utils/headers'
import { sanitizeHtml } from '../utils/sanitize-html'
const API_BASE = import.meta.env.VITE_API_BASE || "";
const {
@@ -123,7 +124,7 @@ const getOpenSettings = async (message, notification) => {
notification.info({
content: () => {
return h("div", {
innerHTML: announcement.value
innerHTML: sanitizeHtml(announcement.value)
});
}
});

View File

@@ -0,0 +1,25 @@
// @vitest-environment jsdom
import { describe, expect, it } from 'vitest';
import { sanitizeHtml } from '../sanitize-html';
describe('sanitizeHtml', () => {
it('preserves safe announcement markup', () => {
expect(sanitizeHtml('<strong>Notice</strong>')).toBe('<strong>Notice</strong>');
});
it('removes executable markup and unsafe attributes', () => {
const sanitized = sanitizeHtml(
'<script>alert(1)</script><img src="x" onerror="alert(1)">'
);
expect(sanitized).not.toContain('<script');
expect(sanitized).not.toContain('onerror');
expect(sanitized).toContain('<img src="x">');
});
it('returns an empty string for non-string values', () => {
expect(sanitizeHtml(null)).toBe('');
expect(sanitizeHtml({ value: '<strong>unsafe ref</strong>' })).toBe('');
});
});

View File

@@ -0,0 +1,5 @@
import DOMPurify from 'dompurify';
export const sanitizeHtml = (html) => {
return DOMPurify.sanitize(typeof html === 'string' ? html : '');
};

View File

@@ -1,13 +1,16 @@
<script setup>
import { computed } from 'vue'
import { GithubAlt, Discord, Telegram } from '@vicons/fa'
import { useGlobalState } from '../../store'
import { sanitizeHtml } from '../../utils/sanitize-html'
const { announcement } = useGlobalState()
const safeAnnouncement = computed(() => sanitizeHtml(announcement.value))
</script>
<template>
<div class="center">
<n-card :bordered="false" embedded>
<div v-html="announcement"></div>
<div v-html="safeAnnouncement"></div>
<n-button tag="a" target="_blank" href="https://github.com/dreamhunter2333/cloudflare_temp_email">
<template #icon>
<n-icon :component="GithubAlt" />