🐛 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 <niwi@niwi.nz>
This commit is contained in:
Andrey Antukh 2026-05-06 23:11:46 +00:00 committed by Alonso Torres
parent 697a825d76
commit 798ee46b4a
3 changed files with 8 additions and 4 deletions

View File

@ -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)

View File

@ -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);
}
/**

View File

@ -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<void> {
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();
});
});