CVE-2021-32851: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Mind-elixir is a free, open source mind map core. Prior to version 0.18.1, mind-elixir is prone to cross-site scripting when handling untrusted menus. This issue is patched in version 0.18.1
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2023-02-20T22:15Z
Repository: https://github.com/ssshooter/mind-elixir-core
Language: TypeScript
Files changed: package.json, readme.md, src/plugin/contextMenu.ts, src/plugin/nodeMenu.ts, src/plugin/toolBar.ts, src/utils/dom.ts, src/utils/index.ts
Commit date: 2021-11-25 15:03:34 +0800
Commit message: fix: prevent XSS

Diff: diff --git a/package.json b/package.json
index 3621c61..7c93416 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
   "name": "mind-elixir",
-  "version": "0.18.0",
+  "version": "0.18.1",
   "description": "Mind elixir is a free open source mind map core.",
   "main": "dist/MindElixir.js",
   "scripts": {
     "start": "webpack-dev-server --mode development --env.dist=0",
     "startd": "webpack-dev-server --mode development --env.dist=1",
diff --git a/readme.md b/readme.md
index 8077059..f7f907f 100644
--- a/readme.md
+++ b/readme.md
@@ -49,11 +49,10 @@ import MindElixir, { E } from 'mind-elixir'
 ```
 
 #### Script tag
 
 ```html
-<script src="https://cdn.jsdelivr.net/npm/regenerator-runtime"></script>
 <script src="https://cdn.jsdelivr.net/npm/mind-elixir/dist/mind-elixir.js"></script>
 ```
 
 ### HTML structure
 
diff --git a/src/plugin/contextMenu.ts b/src/plugin/contextMenu.ts
index 07d40be..67cb637 100644
--- a/src/plugin/contextMenu.ts
+++ b/src/plugin/contextMenu.ts
@@ -1,18 +1,19 @@
 import i18n from '../i18n'
+import { encodeHTML } from '../utils/index'
 
 export default function(mind, option) {
   const createTips = words => {
     const div = document.createElement('div')
-    div.innerHTML = words
+    div.innerText = words
     div.style.cssText = 'position:absolute;bottom:20px;left:50%;transform:translateX(-50%);'
     return div
   }
   const createLi = (id, name, keyname) => {
     const li = document.createElement('li')
     li.id = id
-    li.innerHTML = `<span>${name}</span><span>${keyname}</span>`
+    li.innerHTML = `<span>${encodeHTML(name)}</span><span>${encodeHTML(keyname)}</span>`
     return li
   }
   const locale = i18n[mind.locale] ? mind.locale : 'en'
 
   const add_child = createLi('cm-add_child', i18n[locale].addChild, 'tab')
diff --git a/src/plugin/nodeMenu.ts b/src/plugin/nodeMenu.ts
index 2304add..ba6c13d 100644
--- a/src/plugin/nodeMenu.ts
+++ b/src/plugin/nodeMenu.ts
@@ -1,11 +1,10 @@
 import i18n from '../i18n'
 
-const createDiv = (id, name) => {
+const createDiv = (id) => {
   const div = document.createElement('div')
   div.id = id
-  div.innerHTML = `<span>${name}</span>`
   return div
 }
 
 const colorList = [
   '#2c3e50',
@@ -29,13 +28,13 @@ const colorList = [
 ]
 
 export default function(mind) {
   const locale = i18n[mind.locale] ? mind.locale : 'en'
   let bgOrFont
-  const styleDiv = createDiv('nm-style', 'style')
-  const tagDiv = createDiv('nm-tag', 'tag')
-  const iconDiv = createDiv('nm-icon', 'icon')
+  const styleDiv = createDiv('nm-style')
+  const tagDiv = createDiv('nm-tag')
+  const iconDiv = createDiv('nm-icon')
 
   styleDiv.innerHTML = `
       <div class="nm-fontsize-container">
         ${['15', '24', '32']
     .map(size => {
@@ -58,16 +57,12 @@ export default function(mind) {
       <div class="bof">
       <span class="font">${i18n[locale].font}</span>
       <span class="background">${i18n[locale].background}</span>
       </div>
   `
-  tagDiv.innerHTML = `
-      ${i18n[locale].tag}<input class="nm-tag" tabindex="-1" placeholder="${i18n[locale].tagsSeparate}" /><br>
-  `
-  iconDiv.innerHTML = `
-      ${i18n[locale].icon}<input class="nm-icon" tabindex="-1" placeholder="${i18n[locale].iconsSeparate}" /><br>
-  `
+  tagDiv.innerHTML = `${i18n[locale].tag}<input class="nm-tag" tabindex="-1" placeholder="${i18n[locale].tagsSeparate}" /><br>`
+  iconDiv.innerHTML = `${i18n[locale].icon}<input class="nm-icon" tabindex="-1" placeholder="${i18n[locale].iconsSeparate}" /><br>`
 
   const menuContainer = document.createElement('nmenu')
   menuContainer.innerHTML = `
   <div class="button-container"><svg class="icon" aria-hidden="true">
   <use xlink:href="#icon-close"></use>
diff --git a/src/plugin/toolBar.ts b/src/plugin/toolBar.ts
index 2bc5095..206ca81 100644
--- a/src/plugin/toolBar.ts
+++ b/src/plugin/toolBar.ts
@@ -12,11 +12,11 @@ function createToolBarRBContainer(mind) {
   const fc = createButton('fullscreen', 'full')
   const gc = createButton('toCenter', 'living')
   const zo = createButton('zoomout', 'move')
   const zi = createButton('zoomin', 'add')
   const percentage = document.createElement('span')
-  percentage.innerHTML = '100%'
+  percentage.innerText = '100%'
   toolBarRBContainer.appendChild(fc)
   toolBarRBContainer.appendChild(gc)
   toolBarRBContainer.appendChild(zo)
   toolBarRBContainer.appendChild(zi)
   // toolBarRBContainer.appendChild(percentage)
diff --git a/src/utils/dom.ts b/src/utils/dom.ts
index 475c567..fe1725c 100644
--- a/src/utils/dom.ts
+++ b/src/utils/dom.ts
@@ -1,9 +1,9 @@
 import { LEFT, RIGHT, SIDE } from '../const'
 import vari from '../var'
 import { NodeObj } from '../index'
-
+import { encodeHTML } from '../utils/index'
 export type Top = HTMLElement
 
 export type Group = HTMLElement
 
 export interface Topic extends HTMLElement {
@@ -34,11 +34,11 @@ export const createGroup = function(nodeObj: NodeObj) {
   }
   return { grp, top }
 }
 
 export const shapeTpc = function(tpc: Topic, nodeObj: NodeObj) {
-  tpc.innerHTML = nodeObj.topic
+  tpc.innerText = nodeObj.topic
 
   if (nodeObj.style) {
     tpc.style.color = nodeObj.style.color || 'inherit'
     tpc.style.background = nodeObj.style.background || 'inherit'
     tpc.style.fontSize = nodeObj.style.fontSize + 'px'
@@ -54,27 +54,27 @@ export const shapeTpc = function(tpc: Topic, nodeObj: NodeObj) {
   // }
   if (nodeObj.hyperLink) {
     const linkContainer = $d.createElement('a')
     linkContainer.className = 'hyper-link'
     linkContainer.target = '_blank'
-    linkContainer.innerHTML = '🔗'
+    linkContainer.innerText = '🔗'
     linkContainer.href = nodeObj.hyperLink
     tpc.appendChild(linkContainer)
   }
   if (nodeObj.icons) {
     const iconsContainer = $d.createElement('span')
     iconsContainer.className = 'icons'
     iconsContainer.innerHTML = nodeObj.icons
-      .map(icon => `<span>${icon}</span>`)
+      .map(icon => `<span>${encodeHTML(icon)}</span>`)
       .join('')
     tpc.appendChild(iconsContainer)
   }
   if (nodeObj.tags) {
     const tagsContainer = $d.createElement('div')
     tagsContainer.className = 'tags'
     tagsContainer.innerHTML = nodeObj.tags
-      .map(tag => `<span>${tag}</span>`)
+      .map(tag => `<span>${encodeHTML(tag)}</span>`)
       .join('')
     tpc.appendChild(tagsContainer)
   }
 }
 
@@ -108,11 +108,11 @@ export function createInputDiv(tpc: Topic) {
   console.time('createInputDiv')
   if (!tpc) return
   let div = $d.createElement('div')
   const origin = tpc.childNodes[0].textContent as string
   tpc.appendChild(div)
-  div.innerHTML = origin
+  div.innerText = origin
   div.contentEditable = 'true'
   div.spellcheck = false
   div.style.cssText = `min-width:${tpc.offsetWidth - 8}px;`
   if (this.direction === LEFT) div.style.right = '0'
   div.focus()
@@ -159,11 +159,11 @@ export function createInputDiv(tpc: Topic) {
 }
 
 export const createExpander = function(expanded: boolean | undefined): Expander {
   const expander: Expander = $d.createElement('epd')
   // 包含未定义 expanded 的情况，未定义视为展开
-  expander.innerHTML = expanded !== false ? '-' : '+'
+  expander.innerText = expanded !== false ? '-' : '+'
   expander.expanded = expanded !== false
   expander.className = expanded !== false ? 'minus' : ''
   return expander
 }
 
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 0a8904c..5c41ad2 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,8 +1,12 @@
 import vari from '../var'
 import { NodeObj } from '../index'
 
+export function encodeHTML(s) {
+  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;')
+}
+
 export const isMobile = (): boolean =>
   /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
     navigator.userAgent
   )
 

Vulnerable code:   "version": "0.18.0",
<script src="https://cdn.jsdelivr.net/npm/regenerator-runtime"></script>
    div.innerHTML = words
    li.innerHTML = `<span>${name}</span><span>${keyname}</span>`
const createDiv = (id, name) => {
  div.innerHTML = `<span>${name}</span>`
  const styleDiv = createDiv('nm-style', 'style')
  const tagDiv = createDiv('nm-tag', 'tag')
  const iconDiv = createDiv('nm-icon', 'icon')
  tagDiv.innerHTML = `
      ${i18n[locale].tag}<input class="nm-tag" tabindex="-1" placeholder="${i18n[locale].tagsSeparate}" /><br>
  `
  iconDiv.innerHTML = `
      ${i18n[locale].icon}<input class="nm-icon" tabindex="-1" placeholder="${i18n[locale].iconsSeparate}" /><br>
  `
  percentage.innerHTML = '100%'

  tpc.innerHTML = nodeObj.topic
    linkContainer.innerHTML = '🔗'
      .map(icon => `<span>${icon}</span>`)
      .map(tag => `<span>${tag}</span>`)
  div.innerHTML = origin
  expander.innerHTML = expanded !== false ? '-' : '+'
Fixed code:   "version": "0.18.1",
import { encodeHTML } from '../utils/index'
    div.innerText = words
    li.innerHTML = `<span>${encodeHTML(name)}</span><span>${encodeHTML(keyname)}</span>`
const createDiv = (id) => {
  const styleDiv = createDiv('nm-style')
  const tagDiv = createDiv('nm-tag')
  const iconDiv = createDiv('nm-icon')
  tagDiv.innerHTML = `${i18n[locale].tag}<input class="nm-tag" tabindex="-1" placeholder="${i18n[locale].tagsSeparate}" /><br>`
  iconDiv.innerHTML = `${i18n[locale].icon}<input class="nm-icon" tabindex="-1" placeholder="${i18n[locale].iconsSeparate}" /><br>`
  percentage.innerText = '100%'
import { encodeHTML } from '../utils/index'
  tpc.innerText = nodeObj.topic
    linkContainer.innerText = '🔗'
      .map(icon => `<span>${encodeHTML(icon)}</span>`)
      .map(tag => `<span>${encodeHTML(tag)}</span>`)
  div.innerText = origin
  expander.innerText = expanded !== false ? '-' : '+'
export function encodeHTML(s) {
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;')
}