fix(security): bind internal API and editor servers to loopback - #366
khawarahemad wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The new IPv4-only loopback binding can break existing localhost call sites (IPv6 ::1 resolution) and should be reconciled for compatibility.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR hardens the app’s internal HTTP(S) surfaces by binding the internal Express API server and the Message Editor static server to the IPv4 loopback interface, preventing LAN-accessible exposure of local-only endpoints.
Changes:
- Bind the HTTPS internal API server listener to
127.0.0.1explicitly. - Bind the Message Editor Express static server listener to
127.0.0.1explicitly.
File summaries
| File | Description |
|---|---|
| src/AppCore/APIServer.ts | Explicitly binds the internal HTTPS API server to 127.0.0.1 instead of all interfaces. |
| src/AppCore/MessageEditorServer.ts | Explicitly binds the Message Editor static server to 127.0.0.1 instead of all interfaces. |
Review details
Suppressed comments (2)
src/AppCore/MessageEditorServer.ts:32
- The editor server binds to 127.0.0.1, but the log message still says "API Server" and points to "localhost". This is misleading for debugging and can confuse which server is actually running where.
logger.log(`API Server listening on http://localhost:${port}`);
src/AppCore/APIServer.ts:96
- Binding the API server to 127.0.0.1 is good for LAN isolation, but there are still call sites using
https://localhost:${this.port}(e.g., src/AppCore/index.ts:414, 437). If those URLs resolve to ::1 first, requests may fail or be delayed because this listener is IPv4-only. Consider updating those URLs to 127.0.0.1 or adding an IPv6 loopback listener (::1) for compatibility.
server.listen(0, "127.0.0.1").once("listening", callback);
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| export default async function startEditor (): Promise<number> { | ||
| return new Promise((resolve, reject) => { | ||
| const server = app.listen(0, () => { | ||
| const server = app.listen(0, "127.0.0.1", () => { |
| @@ -93,7 +93,7 @@ export default async function startAppServer (): Promise<number> { | |||
| resolve(address.port); | |||
| logger.log(`API Server listening on https://localhost:${address.port}`); | |||
|
I don't have a Linux box/VM to test Copilot's theory, but it's very likely going to throw some weird exceptions |
Summary
This PR restricts the internal Express API server and Message Editor static server to listen exclusively on the local loopback interface (
127.0.0.1).Impact
server.listen(0)does not constrain the listener to loopback; in the affected runtime, the server was reachable through the LAN-facing interface. Other devices on the same local network could query internal endpoints (such as/api/v9/users/@me/settings-proto/*) and access or modify local settings.Root Cause
In
src/AppCore/APIServer.tsandsrc/AppCore/MessageEditorServer.ts,server.listen(0)/app.listen(0)omitted an explicit host parameter, allowing incoming connections from non-loopback network interfaces.Fix
Explicitly pass
"127.0.0.1"toserver.listen(0, "127.0.0.1")inAPIServer.tsandapp.listen(0, "127.0.0.1")inMessageEditorServer.ts.Validation
npm run test:typescriptandnpm run build:ts.curl -k https://127.0.0.1:<port>/ping->HTTP 200 OKcurl -k --connect-timeout 2 https://<LAN_IP>:<port>/ping-> Connection timed out / refused