Complexity = More Vulnerabilities?
Preface
In today’s tech landscape, integrating front-end and back-end technologies often necessitates complex systems. However, these intricate systems can also introduce new and exotic vulnerabilities. In this post, we’ll explore a case where a sophisticated yet complex system in the widely-used Wiki.js framework led to a critical 0-day vulnerability—CVE-2024-34710.
Background
Wiki.js is a powerful wiki framework built with Vue.js for the front end and Node.js for the back end. It includes a custom rendering system that transforms Markdown into HTML, enhancing content creation flexibility and usability. However, this intricate system brings unique challenges and potential security risks.
In this post, we will focus on the rendering system, examining its complexity and how it led to a CSTI (Client-Side Template Injection) 0-day vulnerability, which in turn resulted in a Stored XSS (Cross-Site Scripting) issue.
Rendering Process
The Wiki.JS framework allowed you to translate a markdown formatted file into HTML when rendered, this was done through a custom rendering engine that would have a pipeline where different modules would be executed hence affecting data in the pipeline:

let output = decodeEscape($.html('body').replace('<body>', '').replace('</body>', ''))
for (let child of _.sortBy(_.filter(this.children, ['step', 'post']), ['order'])) {
const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`)
output = await renderer.init(output, child.config)
}
The above code would iterate through each child module registered while using the output for each of the modules in as input for the next.
Each module called would affect the data in the pipeline in a certain manner, the ones we will be focusing on specifically are:
- html-core : which would handle the calling and execution of modules.
- html-security : which would employ DomPurify to sanitize user supplied input.
Discovery
During an internal assessment of a Wiki.JS instance, multiple XSS test cases were ran all seeming to fail due to DomPurify removing any seemingly malicious content supplied. Some CSTI test cases were ran as well, but failed… this was due to a function within the html-core module that would escape any instances of mustache brackets needed to perform CSTI:
function iterateMustacheNode (node) {
const list = $(node).contents().toArray()
list.forEach(item => {
if (item && item.type === 'text') {
const rawText = $(item).text().replace(/\r?\n|\r/g, '')
if (mustacheRegExp.test(rawText)) {
$(item).parent().attr('v-pre', true)
}
} else {
iterateMustacheNode(item)
$(elm).attr('v-pre', true)
}
})
The code snippet in question was designed to look for any mustache brackets and add the v-pre
attribute to the parent tag. This attribute signals Vue.js to skip rendering any template code within that tag, as outlined in the Vue.js documentation.
However, there was an issue with the order of execution during the rendering process. Specifically, the function that added the v-pre
attribute was executed before the html-security
module, which includes the DomPurify library. DomPurify is a well-known sanitization tool that cleans and escapes XSS payloads from wiki entry contents.
One notable caveat of DomPurify is that it removes any invalid HTML tags during its sanitation process. Analyzing the source code of the rendering engine, a key issue became apparent: the order in which these processes were executed.
To recap, the v-pre
attribute was added to HTML tags to instruct Vue.js to skip template code rendering. Following this, the html-security
module would sanitize the content to prevent XSS attacks while also removing invalid tags. The problem arises because invalid tags removed by DomPurify might inadvertently affect how the v-pre
attribute and template code are handled.
In a perfect world the normal rendering process would look something like this:
- Input is given:
\<h1> New Page \</h1>
\<div>
\<p> {{ 7 * 7 }} \</p>
\</div>
- Input would be given to the html-core module that would escape the template code:
\<h1> New Page \</h1>
\<div v-pre>
\<p> {{ 7 * 7 }} \</p>
\</div>
- After that, it would be passed along to html-security with no issues and the rendering would continue as normal.
Now knowing what DomPurify does to invalid tags, and knowing the order in which these security functions are executed in we could come up with a rather nasty but cool payload that would exploit this. Consider the following:
- Input is given:
\<h1> New Page \</h1>
\<xyz>
\<p> {{ 7 * 7 }} \</p>
\</xyz>
- Input would be given to the html-core module that would escape the template code:
\<h1> New Page \</h1>
\<xyz v-pre>
\<p> {{ 7 * 7 }} \</p>
\</xyz>
- After that, it would be passed along to html-security that would sanitize the content, leading to the xyz tag being removed:
\<h1> New Page \</h1>
\<p> {{ 7 * 7 }} \</p>
As shown in the flow above, after step 2 the template code would be rendered safe and not executed, although due to the order modules were being executed in the very next function executed on the content would be the above mentioned html-security module that would remove invalid tags, this lead to the previously safe template code to be weaponized again due to the invalid tag containing v-pre
being removed.
There we have it, a valid CSTI payload…
Impact
Since session identifiers were stored without the HTTPOnly
attribute, they could be accessed via JavaScript, such as through document.cookie
. This significantly amplified the impact of the vulnerability, allowing attackers to harvest session identifiers and potentially lead to mass account takeovers.
The exploitation was facilitated by using the well-known constructor.constructor('document.cookie')()
template code. This technique enabled the execution of JavaScript within the application’s context, further compromising security.
Remediation
The remediation for this was quite simple, we just have to address the root cause mentioned above, the order in which we were escaping mustache expressions then feeding the content to DomPurify. This was evident in the commit that was pushed to address this .
Conclusion
This serves as a reminder to small development teams considering custom and complex systems instead of using existing libraries. It highlights how easy it is to overlook small details in a complex system.
Why reinvent the wheel? This is a question I believe developers should ask themselves when thinking about custom implementations. Rather than creating custom code that might introduce vulnerabilities, using well-established libraries that are trusted by the developer community and have undergone extensive security testing can be a much safer option.
References
Wiki.JS Rendering: https://docs.requarks.io/rendering
Vulnerability Github Advisory: https://github.com/requarks/wiki/security/advisories/GHSA-xjcj-p2qv-q3rf
Vulnerability CVE: https://nvd.nist.gov/vuln/detail/CVE-2024-34710