@jx3box/jx3box-editor 2.2.44 → 2.2.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/js/xss.js +65 -7
- package/package.json +3 -1
package/assets/js/xss.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import sanitizeHtml from "sanitize-html";
|
|
2
|
+
import * as cheerio from "cheerio";
|
|
3
|
+
import postcss from "postcss";
|
|
4
|
+
import safeParser from "postcss-safe-parser";
|
|
2
5
|
|
|
3
6
|
const FORBID = new Set(["script", "object", "embed", "applet", "base", "meta", "link"]);
|
|
4
7
|
|
|
@@ -8,16 +11,67 @@ const EXTRA_TAGS = [
|
|
|
8
11
|
"table", "thead", "tbody", "tr", "th", "td",
|
|
9
12
|
"blockquote", "pre", "code", "hr",
|
|
10
13
|
"video", "source",
|
|
11
|
-
"iframe",
|
|
14
|
+
"iframe", "style",
|
|
15
|
+
"colgroup", "col",
|
|
12
16
|
];
|
|
13
17
|
|
|
18
|
+
// 必须顶层的 at-rule(你说不需要动画,但 keyframes 也可能被编辑器/作者写进来,留着更稳)
|
|
19
|
+
const TOP_LEVEL_AT = new Set(["keyframes", "-webkit-keyframes", "font-face"]);
|
|
20
|
+
|
|
21
|
+
function stripDangerousCss(css) {
|
|
22
|
+
if (!css) return css;
|
|
23
|
+
return css
|
|
24
|
+
.replace(/@import\s+[^;]+;?/gi, "")
|
|
25
|
+
.replace(/url\s*\(\s*[^)]+\s*\)/gi, "")
|
|
26
|
+
.replace(/expression\s*\([^)]*\)/gi, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 暴力把 style 内容 nest 到 .c-article
|
|
30
|
+
function nestCssBrutally(cssText, scope = ".c-article") {
|
|
31
|
+
let css = stripDangerousCss(cssText || "");
|
|
32
|
+
if (!css.trim()) return css;
|
|
33
|
+
|
|
34
|
+
const root = postcss.parse(css, { parser: safeParser });
|
|
35
|
+
|
|
36
|
+
const top = [];
|
|
37
|
+
const rest = [];
|
|
38
|
+
|
|
39
|
+
(root.nodes || []).forEach((node) => {
|
|
40
|
+
if (node.type === "atrule" && TOP_LEVEL_AT.has(String(node.name).toLowerCase())) {
|
|
41
|
+
top.push(node.toString());
|
|
42
|
+
} else {
|
|
43
|
+
rest.push(node.toString());
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const restCss = rest.join("\n").trim();
|
|
48
|
+
if (!restCss) return top.join("\n").trim();
|
|
49
|
+
|
|
50
|
+
return `${top.join("\n")}\n${scope}{\n${restCss}\n}`.trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function nestAllStyleTags(html, scope = ".c-article") {
|
|
54
|
+
const $ = cheerio.load(html, { decodeEntities: false });
|
|
55
|
+
|
|
56
|
+
$("style").each((_, el) => {
|
|
57
|
+
const oldCss = $(el).html() || "";
|
|
58
|
+
const newCss = nestCssBrutally(oldCss, scope);
|
|
59
|
+
$(el).text(newCss);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return $.html();
|
|
63
|
+
}
|
|
64
|
+
|
|
14
65
|
export default function sanitizeRichText(html) {
|
|
15
66
|
if (!html) return html;
|
|
16
67
|
|
|
68
|
+
// 先把 <style> 内容暴力 nest 到 .c-article
|
|
69
|
+
html = nestAllStyleTags(html, ".c-article");
|
|
70
|
+
|
|
17
71
|
const allowedTags = sanitizeHtml.defaults.allowedTags
|
|
18
72
|
.concat(EXTRA_TAGS)
|
|
19
|
-
.filter((t, i, arr) => arr.indexOf(t) === i)
|
|
20
|
-
.filter(t => !FORBID.has(t));
|
|
73
|
+
.filter((t, i, arr) => arr.indexOf(t) === i)
|
|
74
|
+
.filter((t) => !FORBID.has(t));
|
|
21
75
|
|
|
22
76
|
return sanitizeHtml(html, {
|
|
23
77
|
disallowedTagsMode: "discard",
|
|
@@ -29,12 +83,16 @@ export default function sanitizeRichText(html) {
|
|
|
29
83
|
img: ["src", "alt", "title", "width", "height", "class", "style", "loading", "decoding"],
|
|
30
84
|
video: ["controls", "width", "height", "class", "style"],
|
|
31
85
|
source: ["src", "type"],
|
|
32
|
-
iframe: ["src","width","height","frameborder","scrolling","allowfullscreen","sandbox","referrerpolicy","class","style"],
|
|
86
|
+
iframe: ["src", "width", "height", "frameborder", "scrolling", "allowfullscreen", "sandbox", "referrerpolicy", "class", "style"],
|
|
87
|
+
td: ["colspan", "rowspan", "align", "valign", "class", "style"],
|
|
88
|
+
th: ["colspan", "rowspan", "align", "valign", "class", "style"],
|
|
89
|
+
col: ["span", "width", "class", "style"],
|
|
90
|
+
style: ["type", "media"],
|
|
33
91
|
},
|
|
34
92
|
|
|
35
93
|
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
36
94
|
allowProtocolRelative: true,
|
|
37
|
-
allowedSchemesByTag: { img: ["http", "https", "data"],iframe: ["http", "https"]
|
|
95
|
+
allowedSchemesByTag: { img: ["http", "https", "data"], iframe: ["http", "https"] },
|
|
38
96
|
|
|
39
97
|
transformTags: {
|
|
40
98
|
"*": (tagName, attribs) => {
|
|
@@ -43,7 +101,7 @@ export default function sanitizeRichText(html) {
|
|
|
43
101
|
// 移除 on*
|
|
44
102
|
for (const k of Object.keys(out)) if (/^on/i.test(k)) delete out[k];
|
|
45
103
|
|
|
46
|
-
// style
|
|
104
|
+
// style 属性:禁 @import / url(
|
|
47
105
|
if (typeof out.style === "string" && out.style) {
|
|
48
106
|
let s = out.style;
|
|
49
107
|
s = s.replace(/@import\s+[^;]+;?/gi, "");
|
|
@@ -69,4 +127,4 @@ export default function sanitizeRichText(html) {
|
|
|
69
127
|
},
|
|
70
128
|
},
|
|
71
129
|
});
|
|
72
|
-
}
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jx3box/jx3box-editor",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.45",
|
|
4
4
|
"description": "JX3BOX Article & Editor",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
"katex": "^0.13.0",
|
|
53
53
|
"lodash": "^4.17.15",
|
|
54
54
|
"photoswipe": "^4.1.2",
|
|
55
|
+
"postcss": "^8.5.6",
|
|
56
|
+
"postcss-safe-parser": "^7.0.1",
|
|
55
57
|
"prismjs": "^1.20.0",
|
|
56
58
|
"sanitize-html": "^2.17.0",
|
|
57
59
|
"sortablejs": "^1.15.0",
|