From fbfee7c063970472b68f0d1e522f5364cc467c1e Mon Sep 17 00:00:00 2001 From: Cody8722 <150524916+Cody8722@users.noreply.github.com> Date: Tue, 8 Sep 2026 01:35:11 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E5=89=8D=E7=AB=AF=20escapeHtml=20?= =?UTF-8?q?=E8=A3=9C=E4=B8=8A=E5=90=8C=E4=B8=80=E7=A8=AE=E5=9E=8B=E5=88=A5?= =?UTF-8?q?=E7=B9=9E=E9=81=8E=E6=BC=8F=E6=B4=9E=E7=9A=84=E4=BF=AE=E8=A3=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 昨天修後端 scheduleRenderer.js 的 escapeHtml() 型別繞過漏洞後,發現 frontend/src/utils/escapeHtml.js 是完全相同的舊版邏輯(只在 typeof unsafe === 'string' 時才轉義,陣列/物件原樣放行),被 settingsRenderer.js 等 12 個檔案共用來把資料塞進 innerHTML。 目前經過後端 validateSettings() 驗證的資料(task.name/person.name) 保證是字串,這條路徑還打不穿,所以不是已知可利用的漏洞;但這是同一類 設計缺陷的殘留,比照後端做法一併修掉,避免之後其他資料來源(例如 PDF 匯入還原到前端狀態的資料,不見得會重新過一次後端驗證)繞過。 修法跟後端一致:escapeHtml() 一律先 String() 轉字串再轉義(null/ undefined 仍原樣回傳)。新增 escapeHtml.test.js(10 個測試,含陣列/ 物件繞過的回歸測試)。前端 93/93、後端 348/348 全數通過。 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0117v1Qxx7foQEH4cd6pfnLS --- frontend/src/utils/escapeHtml.js | 7 +++++-- frontend/src/utils/escapeHtml.test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 frontend/src/utils/escapeHtml.test.js diff --git a/frontend/src/utils/escapeHtml.js b/frontend/src/utils/escapeHtml.js index 3d56c76..495ba75 100644 --- a/frontend/src/utils/escapeHtml.js +++ b/frontend/src/utils/escapeHtml.js @@ -1,11 +1,14 @@ /** * HTML 轉義,防止 XSS 注入。 + * 一律先用 String() 轉成字串再轉義,不能只在 typeof === 'string' 時才處理—— + * 否則陣列/物件會原樣通過這個函式,後面樣板字串插值時呼叫它們自己的 + * toString()(例如陣列會 join 內容),繞過所有跳脫直接把原始 HTML 注入。 * @param {*} unsafe * @returns {*} */ export const escapeHtml = (unsafe) => { - if (typeof unsafe !== 'string') return unsafe; - return unsafe + if (unsafe === null || unsafe === undefined) return unsafe; + return String(unsafe) .replace(/&/g, '&') .replace(//g, '>') diff --git a/frontend/src/utils/escapeHtml.test.js b/frontend/src/utils/escapeHtml.test.js new file mode 100644 index 0000000..3fec306 --- /dev/null +++ b/frontend/src/utils/escapeHtml.test.js @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; +import { escapeHtml } from './escapeHtml.js'; + +describe('escapeHtml', () => { + it('轉義 &', () => expect(escapeHtml('a&b')).toBe('a&b')); + it('轉義 <', () => expect(escapeHtml('' }); + expect(result).not.toContain('