From 798ee46b4a84ee6dfc756b001f33acbe0280d62f Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 6 May 2026 23:11:46 +0000 Subject: [PATCH] :bug: Bind MCP ReplServer to localhost to prevent unauthenticated RCE The ReplServer Express app was calling `app.listen(port)` with no host argument, causing Node/Express to default to binding on all interfaces (0.0.0.0). Combined with the unauthenticated /execute endpoint, any network peer could POST arbitrary JS and get it run inside the MCP process. Fix: add a `host` parameter (default "localhost") to the ReplServer constructor and pass it to `app.listen`. The call site in PenpotMcpServer now forwards `this.host` (sourced from PENPOT_MCP_SERVER_HOST env var, default "localhost"), so environment- variable overrides continue to work. Signed-off-by: Andrey Antukh --- CHANGES.md | 1 + mcp/packages/server/src/PenpotMcpServer.ts | 2 +- mcp/packages/server/src/ReplServer.ts | 9 ++++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 667d75f097..bf2ae26a95 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ ### :bug: Bugs fixed +- Fix MCP ReplServer binding to all interfaces (0.0.0.0) instead of localhost, allowing unauthenticated RCE - Fix incorrect invitation token handling on register process [Github #9380](https://github.com/penpot/penpot/pull/9380) - Fix incorrect handling of version restore operation [Github #9041](https://github.com/penpot/penpot/pull/9041) - Fix false “text editing” warning when applying tokens [Github #6346](https://github.com/penpot/penpot/issues/9346) diff --git a/mcp/packages/server/src/PenpotMcpServer.ts b/mcp/packages/server/src/PenpotMcpServer.ts index ae95724a09..47a3ee3355 100644 --- a/mcp/packages/server/src/PenpotMcpServer.ts +++ b/mcp/packages/server/src/PenpotMcpServer.ts @@ -92,7 +92,7 @@ export class PenpotMcpServer { this.tools = this.initTools(); this.pluginBridge = new PluginBridge(this, this.webSocketPort); - this.replServer = new ReplServer(this.pluginBridge, this.replPort); + this.replServer = new ReplServer(this.pluginBridge, this.replPort, this.host); } /** diff --git a/mcp/packages/server/src/ReplServer.ts b/mcp/packages/server/src/ReplServer.ts index 1496037521..1894ade79a 100644 --- a/mcp/packages/server/src/ReplServer.ts +++ b/mcp/packages/server/src/ReplServer.ts @@ -17,13 +17,16 @@ export class ReplServer { private readonly logger = createLogger("ReplServer"); private readonly app: express.Application; private readonly port: number; + private readonly host: string; private server: any; constructor( private readonly pluginBridge: PluginBridge, - port: number = 4403 + port: number = 4403, + host: string = "localhost" ) { this.port = port; + this.host = host; this.app = express(); this.setupMiddleware(); this.setupRoutes(); @@ -86,9 +89,9 @@ export class ReplServer { */ public async start(): Promise { return new Promise((resolve) => { - this.server = this.app.listen(this.port, () => { + this.server = this.app.listen(this.port, this.host, () => { this.logger.info(`REPL server started on port ${this.port}`); - this.logger.info(`REPL interface URL: http://${this.pluginBridge.mcpServer.host}:${this.port}`); + this.logger.info(`REPL interface URL: http://${this.host}:${this.port}`); resolve(); }); });