Wiki.js - Template Injection Stored Cross-Site Scripting (CVE-2020-4052)

Jun 15 2020

Wiki.js >2.4.17 was vulnerable to stored cross-site scripting through template injection. This vulnerability existed due to a malicious payload in a top-level text element bypassing the intended protection mechanisms.

Date Released: 15/06/2020
Author: Denis Andzakovic
Project Website: https://wiki.js.org/
Affected Software: Wiki.js > 2.4.17
Patched Version: 2.4.107
GHSA: GHSA-9jgg-4xj2-vjjj
CVE: CVE-2020-4052

Details

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. The following figure shows an example Wiki.js page (using the raw-HTML editor mode) which executed attacker provided JavaScript:

<h1>Title</h1>

<p>Some text here</p> {{constructor.constructor('alert("Wiki.JS Stored XSS")')()&#x7d&#x7d

The following screenshot shows the POC above executing:

WikiJS XSS

This vulnerability existed due to the HTML rendering logic incorrectly handling curly-braces in a top-level text element. The protection was intended to match on curly braces and insert a v-pre attribute into the parent tag. By injecting the curly braces into a top level text element, the protection was bypassed and the raw payload returned to the user, resulting in stored cross-site scripting. The following snippet shows the code responsible for escaping curly braces:

232 
233     // --------------------------------
234     // Escape mustache expresions
235     // --------------------------------
236 
237     function iterateMustacheNode (node) {
238       const list = $(node).contents().toArray()
239       list.forEach(item => {
240         if (item.type === 'text') {
241           const rawText = $(item).text()
242           if (rawText.indexOf('{{') >= 0 && rawText.indexOf('}}') > 1) {
243             $(item).parent().attr('v-pre', true)
244           }
245         } else {
246           iterateMustacheNode(item)
247         }
248       })
249     }
250     iterateMustacheNode($.root())
251
252     // --------------------------------
253     // STEP: POST
254     // --------------------------------
255
256     let output = decodeEscape($.html('body').replace('<body>', '').replace('</body>', ''))
257

Timeline

12/06/2020 - Advisory sent to Requarks
13/06/2020 - GHSA created
13/06/2020 - Fix created
15/06/2020 - Fix merged to master branch
15/06/2020 - Advisory release