CVE-2020-4052: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: In Wiki.js before 2.4.107, there is a stored cross-site scripting through template injection. This vulnerability exists due to an insecure validation mechanism intended to insert v-pre tags into rendered HTML elements which contain curly-braces. By creating a crafted wiki page, a malicious Wiki.js user may stage a stored cross-site scripting attack. This allows the attacker to execute malicious JavaScript when the page is viewed by other users. This has been patched in 2.4.107.
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: 2020-06-16T22:15Z
Repository: https://github.com/Requarks/wiki
Language: JavaScript
Files changed: server/modules/rendering/html-core/renderer.js
Commit date: 2020-06-13 21:43:21 -0400
Commit message: Merge pull request from GHSA-9jgg-4xj2-vjjj


Diff: diff --git a/server/modules/rendering/html-core/renderer.js b/server/modules/rendering/html-core/renderer.js
index f080d3967..a1f3690ff 100644
--- a/server/modules/rendering/html-core/renderer.js
+++ b/server/modules/rendering/html-core/renderer.js
@@ -2,10 +2,12 @@ const _ = require('lodash')
 const cheerio = require('cheerio')
 const uslug = require('uslug')
 const pageHelper = require('../../../helpers/page')
 const URL = require('url').URL
 
+const mustacheRegExp = /(\{|&#x7b;?){2}(.+?)(\}|&#x7d;?){2}/i
+
 /* global WIKI */
 
 module.exports = {
   async render() {
     const $ = cheerio.load(this.input, {
@@ -228,20 +230,30 @@ module.exports = {
       $(elm).prepend(`<a class="toc-anchor" href="#${headerSlug}">&#xB6;</a> `)
 
       headers.push(headerSlug)
     })
 
+    // --------------------------------
+    // Wrap root text nodes
+    // --------------------------------
+
+    $('body').contents().toArray().forEach(item => {
+      if (item.type === 'text' && item.parent.name === 'body') {
+        $(item).wrap('<div></div>')
+      }
+    })
+
     // --------------------------------
     // Escape mustache expresions
     // --------------------------------
 
     function iterateMustacheNode (node) {
       const list = $(node).contents().toArray()
       list.forEach(item => {
         if (item.type === 'text') {
           const rawText = $(item).text()
-          if (rawText.indexOf('{{') >= 0 && rawText.indexOf('}}') > 1) {
+          if (mustacheRegExp.test(rawText)) {
             $(item).parent().attr('v-pre', true)
           }
         } else {
           iterateMustacheNode(item)
         }

Vulnerable code:           if (rawText.indexOf('{{') >= 0 && rawText.indexOf('}}') > 1) {
Fixed code: const mustacheRegExp = /(\{|&#x7b;?){2}(.+?)(\}|&#x7d;?){2}/i

    // --------------------------------
    // Wrap root text nodes
    // --------------------------------

    $('body').contents().toArray().forEach(item => {
      if (item.type === 'text' && item.parent.name === 'body') {
        $(item).wrap('<div></div>')
      }
    })

          if (mustacheRegExp.test(rawText)) {