Add escapeHtml utility and apply to all user-controllable SweetAlert2 html

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 07:52:26 +08:00
parent 954b41b555
commit 5be29ddd51
3 changed files with 49 additions and 5 deletions

View File

@@ -4,6 +4,7 @@ import ConformanceStore from '@/stores/conformance.js';
import FilesStore from '@/stores/files.js';
import PageAdminStore from '@/stores/pageAdmin.js';
import { useModalStore } from '@/stores/modal.js';
import { escapeHtml } from '@/utils/escapeHtml.js';
const customClass = {
container: '!z-[99999]',
@@ -73,7 +74,7 @@ export async function savedSuccessfully(value) {
value = value || '';
await Swal.fire({
title: 'SAVE COMPLETE',
html: `<span class="text-primary">${value}</span> has been saved in Lucia.`,
html: `<span class="text-primary">${escapeHtml(value)}</span> has been saved in Lucia.`,
timer: 3000, // 停留 3 秒後自動關閉
showConfirmButton: false,
icon: 'success',
@@ -268,7 +269,7 @@ export async function uploadFailedFirst(failureType, failureMsg, failureLoc) {
value = 'File is not in csv format.';
break;
default:
value = failureMsg;
value = escapeHtml(failureMsg);
break;
}
await Swal.fire({
@@ -297,10 +298,10 @@ export async function uploadFailedSecond(detail) {
manySrt = 'There are more errors.';
break;
case 'unrecognized':
content = `<li>Data unregnizable in Status Column: (Row #${i.loc[1]}, "${i.input}")</li>`;
content = `<li>Data unregnizable in Status Column: (Row #${i.loc[1]}, "${escapeHtml(i.input)}")</li>`;
break;
case 'malformed':
content = `<li>Data malformed in Timestamp Column: (Row #${i.loc[1]}, "${i.input}")</li>`;
content = `<li>Data malformed in Timestamp Column: (Row #${i.loc[1]}, "${escapeHtml(i.input)}")</li>`;
break;
case 'missing':
switch (i.loc[2]) {
@@ -442,7 +443,8 @@ export async function renameModal(rename, type, id, baseName) {
export async function deleteFileModal(files, type, id, name) {
const filesStore = FilesStore();
let htmlText = files.length === 0 ? `Do you really want to delete <span class="text-primary">${name}</span>?` : `<div class="text-left mx-4 space-y-1"><p class="mb-2">Do you really want to delete <span class="text-primary">${name}</span>?</p><p>The following dependent file(s) will also be deleted:</p><ul class="list-disc ml-6">${files}</ul></div>`;
const safeName = escapeHtml(name);
let htmlText = files.length === 0 ? `Do you really want to delete <span class="text-primary">${safeName}</span>?` : `<div class="text-left mx-4 space-y-1"><p class="mb-2">Do you really want to delete <span class="text-primary">${safeName}</span>?</p><p>The following dependent file(s) will also be deleted:</p><ul class="list-disc ml-6">${files}</ul></div>`;
const deleteCustomClass = { ...customClass };
deleteCustomClass.confirmButton = '!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-danger !bg-neutral-10 !border !border-danger';

13
src/utils/escapeHtml.js Normal file
View File

@@ -0,0 +1,13 @@
/**
* Escapes HTML special characters to prevent XSS.
* @param {string} str The string to escape.
* @returns {string} The escaped string.
*/
export function escapeHtml(str) {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}

View File

@@ -0,0 +1,29 @@
import { describe, it, expect } from 'vitest';
import { escapeHtml } from '@/utils/escapeHtml.js';
describe('escapeHtml', () => {
it('escapes ampersand', () => {
expect(escapeHtml('a&b')).toBe('a&amp;b');
});
it('escapes angle brackets', () => {
expect(escapeHtml('<script>')).toBe('&lt;script&gt;');
});
it('escapes double quotes', () => {
expect(escapeHtml('"hello"')).toBe('&quot;hello&quot;');
});
it('escapes single quotes', () => {
expect(escapeHtml("it's")).toBe("it&#039;s");
});
it('escapes all special characters together', () => {
expect(escapeHtml('<img src="x" onerror="alert(\'XSS\')">'))
.toBe('&lt;img src=&quot;x&quot; onerror=&quot;alert(&#039;XSS&#039;)&quot;&gt;');
});
it('returns plain text unchanged', () => {
expect(escapeHtml('hello world')).toBe('hello world');
});
});