From 5955ba86c2c027e94fec8db44d99014c92ab2571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Tue, 3 Mar 2026 10:11:56 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=92=20=E4=BD=BF=E7=94=A8=20DOMPuri?= =?UTF-8?q?fy=20=E6=B8=85=E7=90=86=E5=85=AC=E5=91=8A=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E7=9A=84=20HTML=20=E5=86=85=E5=AE=B9=20#1273?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用 DOMPurify 对服务端下发的公告 HTML 进行白名单过滤,防止潜在的 UI 注入风险。只允许基础标签和安全的 CSS 属性(颜色、字体相关)。 Co-Authored-By: Claude Opus 4.6 --- package.json | 1 + pnpm-lock.yaml | 16 ++++++++++++++++ src/pages/popup/App.tsx | 3 ++- src/pkg/utils/sanitize.ts | 24 ++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 src/pkg/utils/sanitize.ts diff --git a/package.json b/package.json index fb3929cee..1a3132b4d 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "crypto-js": "^4.2.0", "dayjs": "^1.11.13", "dexie": "^4.0.10", + "dompurify": "^3.3.1", "eslint-linter-browserify": "9.26.0", "eventemitter3": "^5.0.1", "fast-xml-parser": "^5.3.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8413c881..d80e4503b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: dexie: specifier: ^4.0.10 version: 4.0.10 + dompurify: + specifier: ^3.3.1 + version: 3.3.1 eslint-linter-browserify: specifier: 9.26.0 version: 9.26.0 @@ -1302,6 +1305,9 @@ packages: '@types/sockjs@0.3.36': resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -2080,6 +2086,9 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -5236,6 +5245,9 @@ snapshots: dependencies: '@types/node': 22.16.2 + '@types/trusted-types@2.0.7': + optional: true + '@types/ws@8.18.1': dependencies: '@types/node': 22.16.2 @@ -6197,6 +6209,10 @@ snapshots: '@babel/runtime': 7.27.6 csstype: 3.1.3 + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 diff --git a/src/pages/popup/App.tsx b/src/pages/popup/App.tsx index 26a42c3c5..f4f356f81 100644 --- a/src/pages/popup/App.tsx +++ b/src/pages/popup/App.tsx @@ -1,4 +1,5 @@ import { Discord, DocumentationSite, ExtVersion, ExtServer } from "@App/app/const"; +import { sanitizeHTML } from "@App/pkg/utils/sanitize"; import { Alert, Badge, Button, Card, Collapse, Dropdown, Menu, Switch, Tooltip } from "@arco-design/web-react"; import { IconBook, @@ -482,7 +483,7 @@ function App() { } + content={
} /> { + if (node instanceof HTMLElement && node.hasAttribute("style")) { + const { style } = node; + for (let i = style.length - 1; i >= 0; i--) { + if (!ALLOWED_CSS_PROPERTIES.includes(style[i])) { + style.removeProperty(style[i]); + } + } + } +}); + +// 对 HTML 进行清理,只保留安全的标签和属性 +export function sanitizeHTML(html: string): string { + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: ["b", "i", "a", "br", "p", "strong", "em", "span"], + ALLOWED_ATTR: ["href", "target", "style"], + }); +} From e3976747149562277d79af1a07c4c7b91e91b103 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:06:39 +0900 Subject: [PATCH 2/2] code update --- src/app/service/service_worker/index.ts | 3 ++- src/pages/popup/App.tsx | 17 ++++++++++++----- src/pkg/config/config.ts | 6 ++++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/app/service/service_worker/index.ts b/src/app/service/service_worker/index.ts index 51f381283..70a7c8d61 100644 --- a/src/app/service/service_worker/index.ts +++ b/src/app/service/service_worker/index.ts @@ -21,6 +21,7 @@ import { FaviconDAO } from "@App/app/repo/favicon"; import { onRegularUpdateCheckAlarm } from "./regular_updatecheck"; import { cacheInstance } from "@App/app/cache"; import { InfoNotification } from "./utils"; +import { sanitizeHTML } from "@App/pkg/utils/sanitize"; // service worker的管理器 export default class ServiceWorkerManager { @@ -115,7 +116,7 @@ export default class ServiceWorkerManager { .then((resp: { data: { [key: string]: any; notice: string; version: string } }) => { const data = resp.data; systemConfig - .getCheckUpdate() + .getCheckUpdate({ sanitizeHTML }) .then((items) => { const isRead = items.notice !== data.notice ? false : items.isRead; systemConfig.setCheckUpdate({ ...data, isRead: isRead }); diff --git a/src/pages/popup/App.tsx b/src/pages/popup/App.tsx index f4f356f81..4dcec595d 100644 --- a/src/pages/popup/App.tsx +++ b/src/pages/popup/App.tsx @@ -271,7 +271,7 @@ function App() { const checkScriptEnableAndUpdate = async () => { const [isEnableScript, checkUpdate] = await Promise.all([ systemConfig.getEnableScript(), - systemConfig.getCheckUpdate(), + systemConfig.getCheckUpdate({ sanitizeHTML }), ]); if (!hookMgr.isMounted) return; setIsEnableScript(isEnableScript); @@ -375,13 +375,16 @@ function App() { ]).then(([resp]: [{ data: { notice: string; version: string } } | null | undefined, any]) => { let newCheckUpdateState = 0; if (resp?.data) { + let notice = ""; + if (typeof resp.data.notice === "string") notice = sanitizeHTML(resp.data.notice); + const version = resp.data.version; setCheckUpdate((items) => { - if (resp.data.version === items.version) { + if (version === items.version) { newCheckUpdateState = 2; return items; } - const isRead = items.notice !== resp.data.notice ? false : items.isRead; - const newCheckUpdate = { ...resp.data, isRead }; + const isRead = items.notice !== notice ? false : items.isRead; + const newCheckUpdate = { version, notice, isRead }; systemConfig.setCheckUpdate(newCheckUpdate); return newCheckUpdate; }); @@ -483,7 +486,11 @@ function App() { } + content={ +
+ } /> [0]>("check_update", { + async getCheckUpdate(opts?: { sanitizeHTML?: (html: string) => string }) { + const result = await this._get[0]>("check_update", { notice: "", isRead: false, version: ExtVersion, }); + if (typeof opts?.sanitizeHTML === "function") result.notice = opts.sanitizeHTML(result.notice); + return result; } setEnableScript(enable: boolean) {