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 17:49:32 +08:00
committed by GitHub
parent 4c1e593d07
commit 8c883b269f
6 changed files with 38 additions and 2 deletions
+2 -1
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)
});
}
});
@@ -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('');
});
});
+5
View File
@@ -0,0 +1,5 @@
import DOMPurify from 'dompurify';
export const sanitizeHtml = (html) => {
return DOMPurify.sanitize(typeof html === 'string' ? html : '');
};
+4 -1
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" />