Skip to content

Commit e576451

Browse files
refactor: improve sanitizeDescriptionHtml
1 parent bd3b9f2 commit e576451

File tree

3 files changed

+114
-22
lines changed

3 files changed

+114
-22
lines changed

resources/dist.rc

1.69 KB
Binary file not shown.

src/celemod-ui/src/components/ModList.tsx

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { useAutoDisableNewMods } from '../states';
1717
import { useGlobalContext } from '../App';
1818
import { PopupContext, createPopup } from './Popup';
1919
import { ProgressIndicator } from './Progress';
20+
import { sanitizeDescriptionHtml } from '../sanitizeDescriptionHtml';
2021
// @ts-ignore
2122
import celemodIcon from '../resources/Celemod.png';
2223

@@ -239,38 +240,26 @@ export const Mod = memo(
239240
useEffect(() => {
240241
if (!refContent.current) return;
241242
refContent.current.innerHTML = '';
242-
// strip all script execution from the description
243-
const div = document.createElement('div');
244-
div.innerHTML = data?.description ?? '';
245-
// @ts-ignore
246-
for (const script of div.querySelectorAll(
247-
'script, iframe, style, link, meta'
248-
))
249-
script.remove();
250-
// @ts-ignore
251-
for (const ele of div.querySelectorAll('*')) {
252-
// remove all event listeners
253-
for (const key in ele) {
254-
if (key.startsWith('on')) {
255-
ele[key] = null;
256-
}
257-
}
258-
}
243+
refContent.current.appendChild(
244+
sanitizeDescriptionHtml(data?.description ?? '')
245+
);
246+
247+
// Keep external links going through the native opener.
259248
// @ts-ignore
260-
for (const a of div.querySelectorAll('a')) {
261-
const url = a.href || a.getAttribute('href');
249+
for (const a of refContent.current.querySelectorAll('a')) {
250+
const url = a.getAttribute('href');
251+
if (!url) continue;
262252
a.href = '#';
263253
a.onclick = (e: any) => {
264254
e.preventDefault();
265255
e.stopPropagation();
266256
callRemote('open_url', url);
267257
};
268258
}
259+
269260
// @ts-ignore
270-
for (const img of div.querySelectorAll('img'))
261+
for (const img of refContent.current.querySelectorAll('img'))
271262
img.style.maxWidth = '300px';
272-
273-
refContent.current.appendChild(div);
274263
}, [data]);
275264

276265
if (!data)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
const ALLOWED_DESCRIPTION_TAGS = new Set([
2+
'a',
3+
'b',
4+
'blockquote',
5+
'br',
6+
'code',
7+
'div',
8+
'em',
9+
'h1',
10+
'h2',
11+
'h3',
12+
'h4',
13+
'h5',
14+
'h6',
15+
'hr',
16+
'i',
17+
'img',
18+
'li',
19+
'ol',
20+
'p',
21+
'pre',
22+
'span',
23+
'strong',
24+
'sub',
25+
'sup',
26+
'u',
27+
'ul',
28+
]);
29+
30+
const isSafeDescriptionUrl = (url: string, isImage = false) => {
31+
const value = url.trim();
32+
if (!value) return false;
33+
if (value.startsWith('#')) return !isImage;
34+
if (value.startsWith('/')) return true;
35+
if (value.startsWith('./') || value.startsWith('../')) return true;
36+
37+
const lower = value.toLowerCase();
38+
if (lower.startsWith('http://') || lower.startsWith('https://')) return true;
39+
if (!isImage && lower.startsWith('mailto:')) return true;
40+
41+
return false;
42+
};
43+
44+
export const sanitizeDescriptionHtml = (html: string) => {
45+
const container = document.createElement('div');
46+
const fragment = document.createDocumentFragment();
47+
container.innerHTML = html;
48+
49+
const appendSafeNode = (node: Node, parent: Node) => {
50+
if (node.nodeType === Node.TEXT_NODE) {
51+
parent.appendChild(document.createTextNode(node.textContent ?? ''));
52+
return;
53+
}
54+
55+
if (node.nodeType !== Node.ELEMENT_NODE) return;
56+
57+
const source = node as HTMLElement;
58+
const tagName = source.tagName.toLowerCase();
59+
60+
if (!ALLOWED_DESCRIPTION_TAGS.has(tagName)) {
61+
for (const child of Array.from(source.childNodes)) {
62+
appendSafeNode(child, parent);
63+
}
64+
return;
65+
}
66+
67+
const safeElement = document.createElement(tagName);
68+
69+
if (tagName === 'a') {
70+
const href = source.getAttribute('href');
71+
const title = source.getAttribute('title');
72+
if (href && isSafeDescriptionUrl(href)) {
73+
safeElement.setAttribute('href', href);
74+
}
75+
if (title) safeElement.setAttribute('title', title);
76+
}
77+
78+
if (tagName === 'img') {
79+
const src = source.getAttribute('src');
80+
const alt = source.getAttribute('alt');
81+
const title = source.getAttribute('title');
82+
if (!src || !isSafeDescriptionUrl(src, true)) return;
83+
safeElement.setAttribute('src', src);
84+
if (alt) safeElement.setAttribute('alt', alt);
85+
if (title) safeElement.setAttribute('title', title);
86+
}
87+
88+
const clazz = source.getAttribute('class');
89+
if (clazz) safeElement.setAttribute('class', clazz);
90+
91+
for (const child of Array.from(source.childNodes)) {
92+
appendSafeNode(child, safeElement);
93+
}
94+
95+
parent.appendChild(safeElement);
96+
};
97+
98+
for (const node of Array.from(container.childNodes)) {
99+
appendSafeNode(node, fragment);
100+
}
101+
102+
return fragment;
103+
};

0 commit comments

Comments
 (0)