Skip to content

Commit 4617709

Browse files
CodFrmclaudecyfung1031
authored
🔒 使用 DOMPurify 清理公告通知 HTML 内容 (#1274)
* 🔒 使用 DOMPurify 清理公告通知的 HTML 内容 #1273 使用 DOMPurify 对服务端下发的公告 HTML 进行白名单过滤,防止潜在的 UI 注入风险。只允许基础标签和安全的 CSS 属性(颜色、字体相关)。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * code update --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: cyfung1031 <44498510+cyfung1031@users.noreply.github.com>
1 parent 75180b7 commit 4617709

6 files changed

Lines changed: 60 additions & 8 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"crypto-js": "^4.2.0",
3737
"dayjs": "^1.11.13",
3838
"dexie": "^4.0.10",
39+
"dompurify": "^3.3.1",
3940
"eslint-linter-browserify": "9.26.0",
4041
"eventemitter3": "^5.0.1",
4142
"fast-xml-parser": "^5.3.6",

pnpm-lock.yaml

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/service/service_worker/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { FaviconDAO } from "@App/app/repo/favicon";
2121
import { onRegularUpdateCheckAlarm } from "./regular_updatecheck";
2222
import { cacheInstance } from "@App/app/cache";
2323
import { InfoNotification } from "./utils";
24+
import { sanitizeHTML } from "@App/pkg/utils/sanitize";
2425

2526
// service worker的管理器
2627
export default class ServiceWorkerManager {
@@ -115,7 +116,7 @@ export default class ServiceWorkerManager {
115116
.then((resp: { data: { [key: string]: any; notice: string; version: string } }) => {
116117
const data = resp.data;
117118
systemConfig
118-
.getCheckUpdate()
119+
.getCheckUpdate({ sanitizeHTML })
119120
.then((items) => {
120121
const isRead = items.notice !== data.notice ? false : items.isRead;
121122
systemConfig.setCheckUpdate({ ...data, isRead: isRead });

src/pages/popup/App.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Discord, DocumentationSite, ExtVersion, ExtServer } from "@App/app/const";
2+
import { sanitizeHTML } from "@App/pkg/utils/sanitize";
23
import { Alert, Badge, Button, Card, Collapse, Dropdown, Menu, Switch, Tooltip } from "@arco-design/web-react";
34
import {
45
IconBook,
@@ -270,7 +271,7 @@ function App() {
270271
const checkScriptEnableAndUpdate = async () => {
271272
const [isEnableScript, checkUpdate] = await Promise.all([
272273
systemConfig.getEnableScript(),
273-
systemConfig.getCheckUpdate(),
274+
systemConfig.getCheckUpdate({ sanitizeHTML }),
274275
]);
275276
if (!hookMgr.isMounted) return;
276277
setIsEnableScript(isEnableScript);
@@ -374,13 +375,16 @@ function App() {
374375
]).then(([resp]: [{ data: { notice: string; version: string } } | null | undefined, any]) => {
375376
let newCheckUpdateState = 0;
376377
if (resp?.data) {
378+
let notice = "";
379+
if (typeof resp.data.notice === "string") notice = sanitizeHTML(resp.data.notice);
380+
const version = resp.data.version;
377381
setCheckUpdate((items) => {
378-
if (resp.data.version === items.version) {
382+
if (version === items.version) {
379383
newCheckUpdateState = 2;
380384
return items;
381385
}
382-
const isRead = items.notice !== resp.data.notice ? false : items.isRead;
383-
const newCheckUpdate = { ...resp.data, isRead };
386+
const isRead = items.notice !== notice ? false : items.isRead;
387+
const newCheckUpdate = { version, notice, isRead };
384388
systemConfig.setCheckUpdate(newCheckUpdate);
385389
return newCheckUpdate;
386390
});
@@ -482,7 +486,11 @@ function App() {
482486
<Alert
483487
style={{ display: showAlert ? "flex" : "none" }}
484488
type="info"
485-
content={<div dangerouslySetInnerHTML={{ __html: checkUpdate.notice || "" }} />}
489+
content={
490+
<div
491+
dangerouslySetInnerHTML={{ __html: checkUpdate.notice /* notice is already sanitized by dompurify */ }}
492+
/>
493+
}
486494
/>
487495
<Collapse
488496
bordered={false}

src/pkg/config/config.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,12 +393,14 @@ export class SystemConfig {
393393
});
394394
}
395395

396-
getCheckUpdate() {
397-
return this._get<Parameters<typeof this.setCheckUpdate>[0]>("check_update", {
396+
async getCheckUpdate(opts?: { sanitizeHTML?: (html: string) => string }) {
397+
const result = await this._get<Parameters<typeof this.setCheckUpdate>[0]>("check_update", {
398398
notice: "",
399399
isRead: false,
400400
version: ExtVersion,
401401
});
402+
if (typeof opts?.sanitizeHTML === "function") result.notice = opts.sanitizeHTML(result.notice);
403+
return result;
402404
}
403405

404406
setEnableScript(enable: boolean) {

src/pkg/utils/sanitize.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import DOMPurify from "dompurify";
2+
3+
// 允许的安全 CSS 属性白名单
4+
const ALLOWED_CSS_PROPERTIES = ["color", "font-size", "font-weight", "font-style"];
5+
6+
// 过滤不安全的 CSS 属性,只保留白名单中的属性
7+
DOMPurify.addHook("afterSanitizeAttributes", (node) => {
8+
if (node instanceof HTMLElement && node.hasAttribute("style")) {
9+
const { style } = node;
10+
for (let i = style.length - 1; i >= 0; i--) {
11+
if (!ALLOWED_CSS_PROPERTIES.includes(style[i])) {
12+
style.removeProperty(style[i]);
13+
}
14+
}
15+
}
16+
});
17+
18+
// 对 HTML 进行清理,只保留安全的标签和属性
19+
export function sanitizeHTML(html: string): string {
20+
return DOMPurify.sanitize(html, {
21+
ALLOWED_TAGS: ["b", "i", "a", "br", "p", "strong", "em", "span"],
22+
ALLOWED_ATTR: ["href", "target", "style"],
23+
});
24+
}

0 commit comments

Comments
 (0)