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/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 26a42c3c5..4dcec595d 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, @@ -270,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); @@ -374,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; }); @@ -482,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) { diff --git a/src/pkg/utils/sanitize.ts b/src/pkg/utils/sanitize.ts new file mode 100644 index 000000000..264fbb01b --- /dev/null +++ b/src/pkg/utils/sanitize.ts @@ -0,0 +1,24 @@ +import DOMPurify from "dompurify"; + +// 允许的安全 CSS 属性白名单 +const ALLOWED_CSS_PROPERTIES = ["color", "font-size", "font-weight", "font-style"]; + +// 过滤不安全的 CSS 属性,只保留白名单中的属性 +DOMPurify.addHook("afterSanitizeAttributes", (node) => { + 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"], + }); +}