Penpot Patched the MCP Bridge It Missed in May — and the REPL Still Has No Password
In May 2026, the open-source design platform Penpot fixed CVE-2026-45805 — an MCP REPL server that bound to all interfaces and exposed an unauthenticated /execute endpoint, CVSS 8.8, remote code execution for anyone on the same network. The advisory suggested two fixes: bind to the configured host, and add authentication. The project shipped the first one.
On 27 September 2026, NVD published CVE-2026-100868, which is the same class of bug in the component sitting next to it. The MCP module's plugin WebSocket bridge was never wired to a host variable at all, so it kept listening on every interface with no authentication in single-user mode — the default. Penpot 2.18.0, released 23 September 2026, fixes it. What makes this worth a defender's attention is not the CVSS (6.3 medium on CVSS 3.1, 5.3 on CVSS 4.0) but the pattern: this listener was in the blast radius of the May advisory, was not in its diff, and took four more months to find.
One fix, two listeners, and the one that was never gated
Penpot's MCP module runs three network services. The main HTTP server (PenpotMcpServer.ts) binds this.host, default localhost — correct. The REPL server (ReplServer.ts) was corrected in the May fix and, in current 2.18.0 source, calls listen(this.port, this.host) with the same localhost default — correct. The third, PluginBridge.ts, constructed its WebSocket server as:
this.wsServer = new WebSocketServer({ port: port });
The ws library defaults to all interfaces when no host option is supplied. No PENPOT_MCP_SERVER_HOST, no dev-mode flag, no opt-in. And unlike the REPL — which is gated behind PENPOT_MCP_DEVENV/PENPOT_MCP_REPL_ENABLE — this bridge is constructed unconditionally whenever the MCP server is constructed. It ran in the plain, documented "Getting Started" local setup.
Authentication is optional by design here. The connection handler only demands a userToken in multi-user mode; in single-user mode it takes whichever client connects and treats it as the plugin. From there, any MCP tool call that dispatches work — execute_code, export_shape, import_penpot_file — hands the task to that connection. An attacker on the adjacent network impersonates the browser plugin, reads the task payloads the agent is dispatching, and returns forged results the MCP client will treat as ground truth. NVD classifies it CWE-1327, binding to an unrestricted IP address, vector AV:A — adjacent, not internet-wide, which is the honest scoping.
The fix, commit b5274a4 dated 22 September, is one line: new WebSocketServer({ port: port, host: mcpServer.host }). The commit message is candid about what the bug allowed — "exposing unauthenticated task dispatch to the network in single-user mode."
The incomplete-fix advisory is the more useful document
A second advisory in the batch, GHSA-852x-m8p9-558v, is filed explicitly as an incomplete fix of the May RCE, and it is the one to read if you operate Penpot. Its argument: the /execute endpoint still has no authentication, which would normally be acceptable because the REPL is disabled by default and binds localhost — except the project's own shipped tooling re-creates the original exposure.
We verified both claims against release tags rather than taking the advisory's word for it:
mcp/scripts/start-mcp-devenv— the documented devenv launcher — setsPENPOT_MCP_SERVER_HOST=0.0.0.0 PENPOT_MCP_REMOTE_MODE=true PENPOT_MCP_DEVENV=truein 2.17.2. In 2.18.0 it additionally setsPENPOT_MCP_REPL_HOST=0.0.0.0— the new per-service variable is present in the script, pointed at all interfaces. The devenv path still puts an unauthenticated code-execution endpoint on every interface after the upgrade; the difference is that it is now explicit rather than accidental.docker/images/Dockerfile.mcp, the official MCP image, setsENV PENPOT_MCP_SERVER_HOST=0.0.0.0in 2.17.2 — and still does in 2.18.0. That directly contradicts thelocalhostdefault the module's own README documents, and the README's "Beyond Local Execution" section frames0.0.0.0as an operator opt-in with the warning to "use caution in untrusted networks."isRemoteMode()never gates the REPL. The flag exists precisely to add restrictions when "the server is not assumed to be accessed only by a local user on the same machine," and it gates filesystem access — but the REPL is constructed purely fromisReplEnabled(), with no token, remote mode or not.
So the accurate upgrade note is narrow: 2.18.0 closes the WebSocket bridge exposure. It does not add authentication to /execute, and it does not change the container's bind address. If you run the official MCP image or the devenv script, patching does not move your exposure — your network boundary is still the only control.
Seventeen advisories in one day, and one broken version field
Penpot published 17 GitHub security advisories on 25 September — 1 critical, 1 high, 12 medium, 3 low — against a repository that had published seven in total across the rest of 2026. Three deserve separate mention:
- GHSA-4f36-m4hj-cv86 (critical, CVSS 9.9) — authenticated OS command injection in the SVG exporter. A
fill-colorstring such as#000000$(command)stored on a text object reacheschild_process.exec()during export. The advisory credits three independent reporters across June, July and August 2026 — lyhtheori, B1gN0Se and KimiSecurityTeam — for the same bug class. Note a data-quality trap: the advisory prose says affected<= 2.17.1, fixed in 2.17.2, but the machine-readable patched field reads1.17.2. Scanners consuming the structured field will compare against a version that does not exist. - GHSA-wxgm-8qjw-x445 (high) — SSRF guard bypass via IPv6 transition addresses. Java's
InetAddresspredicates do not classify NAT64 (64:ff9b::/96), 6to4 (2002::/16) or Teredo (2001::/32), so64:ff9b::a9fe:a9fereaches 169.254.169.254 through a guard that blocks the literal IPv4 form. The allow/deny logic only applied its extra CIDR checks to 4-byte addresses. - GHSA-cjm5-wh82-hwr2 (CVSS 6.1) — importing a crafted design-tokens file executes JavaScript in the victim's Penpot origin, because the notification component renders its
detailfield through an unconditionaldangerouslySetInnerHTMLthat ignores the component's ownis-htmlflag. Token key paths from the uploaded file are spliced in raw.
The shape here is familiar from DBHub's read-only mode that was dead code and from the MCP Python SDK's WebSocket origin gate: MCP servers accumulate listeners faster than they accumulate authentication, and the safe-by-default posture lives in documentation rather than in code. The CIS MCP server benchmark published this month exists precisely because "which interfaces does each of my MCP listeners bind, and which of them checks a credential" is a question most operators cannot currently answer.
What to do
- Upgrade to 2.18.0, then verify the bind addresses yourself. On the MCP host,
ss -lntpand confirm the plugin bridge, MCP HTTP server and REPL port are on loopback. The upgrade fixes the bridge; it does not fix your container'sENV. - Override
PENPOT_MCP_SERVER_HOSTandPENPOT_MCP_REPL_HOSTexplicitly in your compose or deployment file. Do not inherit the image default. Treat any MCP port reachable off-host as a code-execution port until proven otherwise. - Do not run
start-mcp-devenvon a shared or office network. It binds an unauthenticated/executeto all interfaces by design in 2.18.0. That is a laptop-with-no-neighbours tool. - If you patched only for CVE-2026-45805 in May, re-audit. The sibling listener was never in that diff. When an advisory names one service in a module, inventory every other listener that module opens before you close the ticket.
- Pin the SVG exporter fix by prose, not by the structured field. 2.17.2 is the real fix for the 9.9; a scanner that trusts the advisory's
1.17.2will misreport. Anyone below 2.17.2 should treat the command injection — not the MCP bridge — as the urgent item in this batch. - Extend SSRF denylists to IPv6 transition ranges. NAT64, 6to4 and Teredo bypass language-runtime "is this private" helpers in more codebases than this one. If your agent or webhook stack fetches user-supplied URLs, test
64:ff9b::a9fe:a9feagainst your own guard today.
Sources:
- NVD — CVE-2026-100868 (published 27 September 2026; CVSS 3.1 6.3 AV:A, CVSS 4.0 5.3, CWE-1327; Penpot before 2.18.0)
- GHSA-ch2q-6x56-qg5r — MCP plugin WebSocket bridge binds all interfaces regardless of PENPOT_MCP_SERVER_HOST (published 25 September 2026; affected ≤ 2.17.2, patched 2.18.0)
- GHSA-852x-m8p9-558v — Incomplete fix of GHSA-22qr-rp27-j9wm: devenv script and Docker image reintroduce unauthenticated MCP REPL on 0.0.0.0
- GHSA-22qr-rp27-j9wm / CVE-2026-45805 — MCP REPL server binds 0.0.0.0 with unauthenticated /execute, RCE (CVSS 8.8; published 19 May 2026, patched 2.15.0)
- GHSA-4f36-m4hj-cv86 — Authenticated OS command injection in the Penpot SVG exporter via legacy fill-color (CVSS 9.9; three independent reporters)
- GHSA-wxgm-8qjw-x445 — SSRF guard bypass via IPv6 transition addresses (NAT64/6to4/Teredo)
- GHSA-cjm5-wh82-hwr2 — XSS via crafted design-tokens import rendered through dangerouslySetInnerHTML (CVSS 6.1)
- penpot/penpot commit b5274a4 — Bind MCP PluginBridge WebSocket to configured host (#11605), 22 September 2026
- penpot/penpot release 2.18.0 (published 23 September 2026)
- Penpot MCP module README at 2.18.0 — PENPOT_MCP_SERVER_HOST and PENPOT_MCP_REPL_HOST default to localhost; "Beyond Local Execution"