@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.
Files changed (2) hide show
  1. package/assets/js/xss.js +65 -7
  2. 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 @import / url(
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.44",
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",