Repository navigation
Security
This page describes the threat model of draw.io and the rules that code touching diagram content must follow. It is for anyone who forks the editor, writes plugins or custom shapes, embeds the editor or the viewer, or hosts draw.io. To report a vulnerability, see Reporting a vulnerability.
A diagram is untrusted input. Anyone can craft a .drawio file, a PNG or SVG with an embedded diagram, a URL that opens a diagram, or a message to an embedded editor. The same diagram is rendered in several places, and some of them run inside someone else's page:
| Where a diagram is rendered | Runs in the origin of | Code |
|---|---|---|
| The editor (app.diagrams.net, self-hosted, desktop) | the editor |
js/grapheditor/, js/diagramly/
|
The viewer (viewer.min.js, <div class="mxgraph">) |
the host page | GraphViewer.js |
The lightbox (lightbox=1) |
the host that serves it (viewer.diagrams.net for lightbox links made by the editor) |
GraphViewer.js, App.js
|
Embed mode (embed=1 in an iframe) |
the editor, driven by the host page |
EditorUi.prototype.installMessageHandler in diagramly/EditorUi.js
|
| Exported SVG and HTML | wherever the file is opened or inlined |
Graph.prototype.getSvg, EditorUi export code |
A malicious diagram is therefore a realistic attack: a crafted label in a diagram shown on a wiki page through the viewer is stored cross-site scripting for every reader of that page. Everything in a diagram is attacker-controlled: cell values and labels, custom properties (they become tooltips and placeholders), style strings, links, embedded images and fonts, page names, and the content of imported files (Visio, Lucidchart, Gliffy, Mermaid, CSV and others).
The model keeps labels exactly as written. HTML labels (style html=1) are sanitised only when they are rendered, in the cellRenderer.getLabelValue override that the Graph constructor installs in Graph.js:
- Labels without
html=1are escaped withmxUtils.htmlEntitiesand shown as text. - Labels with
html=1go throughGraph.sanitizeHtml, which callsGraph.domPurifyand so DOMPurify (js/sanitizer/purify.min.js) withwindow.DOM_PURIFY_CONFIG(defined ingrapheditor/Init.js). Hooks registered withDOMPurify.addHookinGraph.jsadd draw.io's own checks, for example on CSS (see Styles and resources). -
<style>elements in labels are scoped withGraph.scopeStyleElementsso that they cannot restyle the page around the diagram.
The editor, the viewer and the exports all render labels through this path, so the plain rendering is safe. The rule is: any code that reads a label itself and puts it into the DOM as HTML must sanitise it itself. That includes graph.getLabel(cell), graph.convertValueToString(cell), cell.value and cell.value.getAttribute('label').
Graph.prototype.getIndexableText is a good example of reading labels correctly:
if (this.isHtmlLabel(cell))
{
tmp.innerHTML = Graph.sanitizeHtml(this.getLabel(cell));
label = mxUtils.extractTextWithWhitespace([tmp]);
}The same applies to custom shapes. A shape that paints a label on its own with c.text(..., format, ...) and format set to 'html' ends up in mxSvgCanvas2D.prototype.createDiv, which assigns the string to innerHTML. Sanitise it first, as Shapes.js does where a shape paints a copy of its label:
var format = mxUtils.getValue(this.style, 'html', '0') == '1' ? 'html' : '';
if (format == 'html')
{
label = Graph.scopeHtmlStyles(Graph.sanitizeHtml(label));
}
c.text(x, y, w, h, label, align, valign, wrap, format, null, false, 0, null);Plain text painted with an empty format is escaped by the canvas and needs nothing extra.
Do not weaken window.DOM_PURIFY_CONFIG. It is defined as window.DOM_PURIFY_CONFIG || {...}, so a host page or PreConfig.js can replace it, but every allowed tag or attribute added there applies to every label of every diagram.
Treat these as injection sinks whenever any part of the string comes from a diagram, a file, a URL, a message or a storage provider:
| Sink | Use instead |
|---|---|
innerHTML, outerHTML, insertAdjacentHTML
|
document.createElement + mxUtils.write(elt, text) or textContent; Graph.sanitizeHtml(html) if markup is really needed |
document.write |
DOM APIs |
setAttribute('href' / 'src', ...), window.open
|
Graph.sanitizeLink and graph.openLink (see Links) |
setAttribute('on...', ...), eval, new Function, setTimeout with a string |
Never with external input |
<script> elements, mxscript(url)
|
Only with URLs of your own deployment |
Canvas text(..., 'html', ...)
|
Graph.sanitizeHtml first |
| HTML strings for tooltips, dialogs, status messages |
mxUtils.htmlEntities(value) for every value |
When you build an HTML string, escape each value. Graph.prototype.getTooltipForCell builds the tooltip of a cell from its custom properties like this:
tip += ((temp[i].name != 'link') ? '<b>' + mxUtils.htmlEntities(temp[i].name) +
':</b> ' : '') + mxUtils.htmlEntities(value) + '\n';Dialog and status APIs that accept HTML do the same with their input, for example EditorUi.prototype.handleError escapes the error message with mxUtils.htmlEntities before it calls showError, and the dialog action of the embed protocol escapes message. ErrorDialog additionally runs the message through Graph.sanitizeHtml. When you add such an API, document whether it takes text or HTML and escape at the caller.
// Unsafe: page names come from the file
div.innerHTML = '<b>' + mxResources.get('pages') + ':</b> ' + page.getName();
// Safe
var b = document.createElement('b');
mxUtils.write(b, mxResources.get('pages') + ':');
div.appendChild(b);
mxUtils.write(div, ' ' + page.getName());Links come from the link attribute of a cell, from <a href> in HTML labels and from custom link actions. Do not build href values or window.open calls by hand; use the helpers in Graph.js:
| Helper | What it does |
|---|---|
Graph.sanitizeLink(href) |
Returns the link if DOMPurify keeps it as the href of an anchor, otherwise null. DOM_PURIFY_CONFIG.ALLOWED_URI_REGEXP rejects any URI that contains javascript:. |
graph.getLinkForCell(cell) |
Returns the link attribute with any leading javascript: removed by mxUtils.removeJavascriptProtocol. Only pass allowUnsafe = true when the value is not used as a link. |
graph.openLink(href, target, allowOpener) |
Sanitises the link, then opens it. New windows get noopener,noreferrer unless allowOpener is true. |
Graph.isSafeLinkTarget(target) |
Accepts only _blank, _self, _top and _parent as targets of label links. |
graph.isCustomLink(href) |
True for data: links, which the app handles itself and never navigates to. |
Graph.prototype.labelLinkClicked shows the pattern: a label link that does not survive Graph.sanitizeLink is sanitised in place, custom links are left to the app, and everything else goes through openLink.
Custom links are data:page/id,... (go to a page, Graph.isPageLink) and data:action/json,{...} (Graph.prototype.handleCustomLink in diagramly/Editor.js). The JSON is parsed and run by Graph.prototype.executeCustomActions, which only knows a fixed set of declarative actions (for example show, hide, toggle, style, select, highlight, scroll, viewbox, page, open, wait and the animation effects). None of them evaluates code, and the open action uses openLink. If you add an action, keep it declarative and route every URL through the same helpers. The action format is documented at Animation actions JSON.
Style strings are untrusted too. Values that make the browser load something need checks:
-
CSS in HTML labels. The DOMPurify hooks in
Graph.jspass everystyleattribute and every<style>element in a label toGraph.isStyleAllowed. It rejects@importand any reference that is not relative or adata:URL, whichever CSS construct carries it. A declaration that fails is removed. Values with characters that would be dropped when the markup is serialised are removed as well, and attribute values that start with markup are dropped. -
Fonts.
fontSourceURLs are checked byGraph.isValidFontUrl(onlyhttp:,https:,data:and relative URLs), and font names and URLs are escaped withGraph.escapeCssStringbefore they are written into@font-facerules. -
SVG
<use>in labels may only reference fragments (#id) of the same document.
If you add a style that loads a resource or ends up in CSS, validate the URL with an allowlist of schemes and escape the value for its context, as Graph.createFontElement does.
The graph library has several switches that would evaluate strings taken from a document. They are off by default and must stay off for untrusted diagrams: mxObjectCodec.allowEval, mxObjectCodec.allowInclude, mxStylesheetCodec.allowEval, mxStencil.allowEval and mxGraphView.prototype.allowEval.
mxUtils.parseXml parses with the browser's DOMParser, which does not load external entities or DTDs. If you read .drawio files outside the browser (Node.js, Java, Python), configure your XML parser to refuse DTDs and external entities; the format never needs them. The structure of the format is described in File format.
Keys from documents and messages must never reach Object.prototype:
-
mxCodeckeeps its ID lookups inObject.create(null)maps, so a cell ID such as__proto__is an ordinary key. -
mxObjectCodec.isPollutionKey(name)rejects__proto__,constructorandprototypeas field names during decoding. -
EditorUi.isPrototypePollutionKey(key)inDiffSync.jsdoes the same for patches that arrive through collaboration and the embed protocol.
Follow the same pattern in new code: use Map or Object.create(null) for lookups keyed by IDs or names from a file, and skip the three dangerous keys when copying properties from parsed JSON or style strings, as Graph.js does when it reads styles:
if (key != '__proto__' && key != 'constructor' && key != 'prototype')
{
result[key] = value;
}A plugin is an ordinary script in the editor page. It has full access to the page, the diagram, the user's signed-in storage and everything the user can do, so only load plugins you have reviewed like your own code. Plugins that show diagram content must follow the rules on this page. Writing plugins is covered in Plugins.
Loading is restricted in App.main (App.js):
| Source | What is loaded |
|---|---|
p=id1;id2 URL parameter |
Only IDs listed in App.pluginRegistry (files in plugins/ of the deployment) |
| Plugins the user added in the Plugins dialog (stored in the settings) | Only URLs on the same origin as the editor (App.isSameDomain), and only the built-in plugins unless window.ALLOW_CUSTOM_PLUGINS is true
|
plugins=0 or offline=1
|
Nothing |
Blocked entries are logged to the console as Blocked plugin: or Unknown plugin:. Leave ALLOW_CUSTOM_PLUGINS at its default false unless you control every script on your origin; with it set, the Plugins dialog lets users add any same-origin script.
When you embed the editor (embed=1), the editor only accepts messages whose evt.source is its parent or opener window, and it posts its replies, including the diagram XML, to that window. Your page must check both the origin and the source of every message it receives, and send its own messages to the editor's origin only:
var editorOrigin = 'https://embed.diagrams.net';
var iframe = document.createElement('iframe');
iframe.src = editorOrigin + '/?embed=1&proto=json&spin=1';
document.body.appendChild(iframe);
window.addEventListener('message', function(evt)
{
if (evt.origin != editorOrigin || evt.source != iframe.contentWindow)
{
return;
}
var msg = JSON.parse(evt.data);
if (msg.event == 'init')
{
iframe.contentWindow.postMessage(JSON.stringify(
{action: 'load', xml: xml}), editorOrigin);
}
else if (msg.event == 'save')
{
// msg.xml is user content: store it, do not insert it as HTML
}
});The full protocol is documented at Embed mode and in Embedding.
Treat the XML, SVG and HTML you get back as user content:
- Display stored diagrams with the viewer or the lightbox, which sanitise labels, rather than with your own rendering of the XML.
- An exported SVG shown with
<img src>cannot run scripts. Inlining SVG markup into your page puts it in your origin; if users can supply the file, sanitise it. -
viewer.min.jsrenders into your page and runs in your origin. If you show diagrams from sources you do not trust, consider the lightbox or the viewer in an iframe on a separate origin, so that the diagram is isolated from your application.
- Keep up to date. Fixes are released in new versions only; there are no backports to older releases (see SECURITY.md). Track the releases and the advisories.
-
Configuration is public.
PreConfig.js,PostConfig.js,DRAWIO_CONFIGand the editor configuration are delivered to every browser that loads the editor. Never put secrets such as API keys there unless every user may see them. -
/proxyis a server-side request surface.ProxyServlet(source) fetches URLs on behalf of the browser (remote images, fonts and diagrams). It answers 404 unless the environment variableENABLE_DRAWIO_PROXY=1is set. When enabled,Utils.validatedAddressonly allowshttpandhttpson default ports (more viaDRAWIO_PROXY_ALLOWED_PORTS), refuses loopback, private, link-local and other reserved addresses (ALLOW_INTERNAL_10_NET=1opens10.0.0.0/8), connects to the address it checked, re-checks each redirect and limits the response size. Enable it only if you need it, do not widen the defaults without a reason, and restrict the server's outbound network access as for any service that makes requests for its users. -
/export(ExportProxyServlet) forwards export requests to the service named by theEXPORT_URLenvironment variable. Point it only at a service you run. -
Expose only what you use. The servlets of the Java back end are mapped in
WEB-INF/web.xml. If your users only need the editor, servesrc/main/webappas static files or block the endpoints you do not need in your reverse proxy. -
Lockdown. The
lockdownURL parameter and thelockdownconfiguration key stop data from leaving the browser except to the chosen storage; see Data governance and lockdown. -
Content-Security-Policy.
index.htmlhas no inline scripts, so a strictscript-srcworks. The policies used for app.diagrams.net and the viewer are written inDevel.jsbetweenCSP-POLICIES-BEGINandCSP-POLICIES-END; development mode applies the development variant as a<meta>tag, and?dev=1&print-csp=1prints the production variants to the console. Use them as a starting point and remove the hosts of the storage backends you do not offer. Addframe-ancestorsto control which sites may frame your editor. The Docker image injects its own policy, whichDRAWIO_CSP_HEADERoverrides.
More on deployment in Self-hosting and Configuration.
Report vulnerabilities privately through GitHub: Security tab > Report a vulnerability. Do not open a public issue, pull request or discussion for a security problem. The policy is in SECURITY.md.
- In scope: the latest version in this repository, or the version deployed at app.diagrams.net if it differs.
- Out of scope: the PlantUML integration and the www.drawio.com web site.
- Include: a description and the impact, the version or URL, steps to reproduce (ideally a minimal proof of concept or sample diagram), and logs or screenshots.
Reports are acknowledged and investigated privately. Please allow reasonable time for a fix before public disclosure; reporters are credited in the advisory unless they prefer to stay anonymous.
Fixed vulnerabilities are published as GitHub security advisories at https://github.com/jgraph/drawio/security/advisories. Check them when you maintain a fork or run an older version: each advisory lists the affected versions.
Describes the dev branch of jgraph/drawio as of release 32.3.0 (October 2026). Internal JavaScript APIs change between releases; check the code of the version you use. · Questions: Discussions · Bugs: Issues · Vulnerabilities: report privately · User docs: drawio.com/docs
Get started
Concepts
Guides
- Self-hosting
- Configuration
- Storage backends
- OneDrive app registration
- Embedding
- Diagrams in GitHub
- Plugins
- Extending the editor
- Shapes and stencils
- Import and export
- Desktop app
- Internationalization
Reference
Project
Elsewhere