Reflected XSS
CVE Request — GHSA-jx8r-j32j-q3mq
Description
Reflected Cross-Site Scripting (XSS) vulnerability in the embedded MCP HTTP server of next-ai-draw-io. The server interpolates the ?mcp= query parameter directly into a <script> block and into raw HTML without sanitization or encoding, allowing arbitrary JavaScript execution in the localhost origin when a victim visits a crafted URL.
Affected Product
| Field | Value |
|---|---|
| Ecosystem | npm |
| Package | @next-ai-drawio/mcp-server |
| Affected versions | <= 0.2.1 (bundled in next-ai-draw-io <= 0.4.16) |
| Patched versions | (pending — to be confirmed once fix is released) |
Weakness
CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-Site Scripting')
Severity (CVSS v3.1)
- Score: 6.1 (Moderate)
- Vector:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Summary
The MCP embedded HTTP server interpolates the ?mcp= query parameter directly into a <script> block and into raw HTML without any sanitization or encoding. An attacker who can trick a user into visiting a crafted http://localhost:<port>/?mcp=… URL causes arbitrary JavaScript to execute in the localhost origin, where it can read and exfiltrate every diagram session stored by the server.
Details
packages/mcp-server/src/http-server.ts extracts the query parameter at line 264:
// line 264
const sessionId = url.searchParams.get("mcp") || ""sessionId is then interpolated unescaped in two places:
Sink 1 — Inside a <script> block (line 700, JS injection)
// line 700 — inside getHtmlPage(sessionId)
const sessionId = "${sessionId}";A value such as ";alert(1);// closes the string literal, injects arbitrary statements, and comments out the remainder of the line.
Sink 2 — Inside HTML markup (line 638, HTML injection)
// line 638
${sessionId ? `<span class="session">${sessionId.slice(-8)}</span>` : ""}A value containing <img src=x onerror=…> is rendered verbatim into the page.
Neither encodeURIComponent, encodeHTML, nor any escaping function is applied before either interpolation.
Proof of Concept
Prerequisites: the MCP HTTP server is running locally (default port range 6002–6020, or any custom port).
- Identify the port (displayed in the VS Code sidebar or MCP server logs).
- Open the following URL in any browser while the server is running:
http://localhost:<PORT>/?mcp=%22%3Balert(document.domain)%3B%2F%2FURL-decoded payload: ";alert(document.domain);//
- The page loads and the browser shows an
alert()dialog displaying the origin (http://localhost:<PORT>) without any user interaction beyond navigating to the URL.
Delivery vector: embed the URL inside a malicious webpage hosted on an attacker-controlled domain:
<iframe src="http://localhost:6002/?mcp=%22%3Balert(document.origin)%3B%2F%2F"></iframe>Or via window.open(…). When the victim visits the page while the MCP server is running, the payload executes automatically.
Impact
| Dimension | Detail |
|---|---|
| Who is affected | Any user running @next-ai-drawio/mcp-server who visits an attacker-controlled webpage while the MCP HTTP server is active |
| Attacker prerequisite | Victim visits a malicious page — no credentials or local access required |
| Consequence | Injected JavaScript executes in the localhost origin and can call any /api/* endpoint (read all diagram XML, session history, model configuration) and exfiltrate data to an external server without further user interaction |
Recommended Fix
HTML-encode sessionId before any interpolation into HTML or JavaScript:
function escapeHtml(s: string): string {
return s.replace(/&/g, "&").replace(/</g, "<")
.replace(/>/g, ">").replace(/"/g, """)
.replace(/'/g, "'")
}
function escapeJs(s: string): string {
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
.replace(/'/g, "\\'").replace(/</g, "\\x3C")
.replace(/\r?\n/g, "\\n")
}
// line 700 — JS context
const sessionId = "${escapeJs(sessionId)}";
// line 638 — HTML context
${sessionId ? `<span class="session">${escapeHtml(sessionId.slice(-8))}</span>` : ""}Reporter: @HK4zCzi (Hồ Việt Khánh)
Source: DayuanJiang/next-ai-draw-io