diff --git a/src/module/alertModal.js b/src/module/alertModal.js index a4a82bf..1fed4bb 100644 --- a/src/module/alertModal.js +++ b/src/module/alertModal.js @@ -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: `${value} has been saved in Lucia.`, + html: `${escapeHtml(value)} 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 = `
  • Data unregnizable in Status Column: (Row #${i.loc[1]}, "${i.input}")
  • `; + content = `
  • Data unregnizable in Status Column: (Row #${i.loc[1]}, "${escapeHtml(i.input)}")
  • `; break; case 'malformed': - content = `
  • Data malformed in Timestamp Column: (Row #${i.loc[1]}, "${i.input}")
  • `; + content = `
  • Data malformed in Timestamp Column: (Row #${i.loc[1]}, "${escapeHtml(i.input)}")
  • `; 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 ${name}?` : `

    Do you really want to delete ${name}?

    The following dependent file(s) will also be deleted:

    `; + const safeName = escapeHtml(name); + let htmlText = files.length === 0 ? `Do you really want to delete ${safeName}?` : `

    Do you really want to delete ${safeName}?

    The following dependent file(s) will also be deleted:

    `; 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'; diff --git a/src/utils/escapeHtml.js b/src/utils/escapeHtml.js new file mode 100644 index 0000000..b3e4fd1 --- /dev/null +++ b/src/utils/escapeHtml.js @@ -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, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/tests/unit/utils/escapeHtml.test.js b/tests/unit/utils/escapeHtml.test.js new file mode 100644 index 0000000..7bc03cc --- /dev/null +++ b/tests/unit/utils/escapeHtml.test.js @@ -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&b'); + }); + + it('escapes angle brackets', () => { + expect(escapeHtml('