Skip to content

Commit 1966067

Browse files
Matthew WangMatthew Wang
authored andcommitted
Prevent RCEs
1 parent b96ab1c commit 1966067

File tree

5 files changed

+190
-50
lines changed

5 files changed

+190
-50
lines changed

server/index.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import { readFileSync, existsSync } from "fs";
1414
import { join, dirname, resolve } from "path";
1515
import { fileURLToPath } from "url";
1616
import { MCPClientManager } from "@/sdk";
17+
import {
18+
buildCorsHeaders,
19+
getAllowedOrigin,
20+
isAllowedHost,
21+
} from "./utils/cors";
1722

1823
const __filename = fileURLToPath(import.meta.url);
1924
const __dirname = dirname(__filename);
@@ -143,6 +148,31 @@ const app = new Hono().onError((err, c) => {
143148
return c.json({ error: "Internal server error" }, 500);
144149
});
145150

151+
// Reject requests that are not targeting the loopback host or that originate
152+
// from unexpected origins. This mitigates the "0.0.0.0 day" class of attacks
153+
// that coerce local services into serving cross-origin traffic.
154+
app.use("*", async (c, next) => {
155+
const hostHeader = c.req.header("x-forwarded-host") || c.req.header("host");
156+
if (!isAllowedHost(hostHeader)) {
157+
appLogger.warn("Blocked request with disallowed host header", {
158+
host: hostHeader,
159+
path: c.req.path,
160+
});
161+
return c.json({ error: "Host not allowed" }, 403);
162+
}
163+
164+
const originHeader = c.req.header("origin");
165+
if (originHeader && !getAllowedOrigin(originHeader)) {
166+
appLogger.warn("Blocked request with disallowed origin", {
167+
origin: originHeader,
168+
path: c.req.path,
169+
});
170+
return c.json({ error: "Origin not allowed" }, 403, { Vary: "Origin" });
171+
}
172+
173+
await next();
174+
});
175+
146176
// Load environment variables early so route handlers can read CONVEX_HTTP_URL
147177
const envFile =
148178
process.env.NODE_ENV === "production"
@@ -218,14 +248,23 @@ app.route("/api/mcp", mcpRoutes);
218248
// We resolve the upstream messages endpoint via sessionId and forward with any injected auth.
219249
// CORS preflight
220250
app.options("/sse/message", (c) => {
221-
return c.body(null, 204, {
222-
"Access-Control-Allow-Origin": "*",
223-
"Access-Control-Allow-Methods": "POST,OPTIONS",
224-
"Access-Control-Allow-Headers":
225-
"Authorization, Content-Type, Accept, Accept-Language",
226-
"Access-Control-Max-Age": "86400",
227-
Vary: "Origin, Access-Control-Request-Headers",
251+
const originHeader = c.req.header("origin");
252+
const { headers, allowedOrigin } = buildCorsHeaders(originHeader, {
253+
allowMethods: "POST,OPTIONS",
254+
allowHeaders:
255+
"Authorization, Content-Type, Accept, Accept-Language, X-MCPJam-Endpoint-Base",
256+
maxAge: "86400",
257+
allowPrivateNetwork: true,
258+
requestPrivateNetwork:
259+
c.req.header("access-control-request-private-network") === "true",
228260
});
261+
262+
if (originHeader && !allowedOrigin) {
263+
return c.json({ error: "Origin not allowed" }, 403, { Vary: "Origin" });
264+
}
265+
266+
headers.Vary = `${headers.Vary}, Access-Control-Request-Headers`;
267+
return c.body(null, 204, headers);
229268
});
230269

231270
// Health check

server/routes/mcp/elicitation.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Hono } from "hono";
22
import type { ElicitResult } from "@modelcontextprotocol/sdk/types.js";
3+
import { buildCorsHeaders } from "../../utils/cors";
34

45
const elicitation = new Hono();
56

@@ -49,6 +50,10 @@ elicitation.use("*", async (c, next) => {
4950

5051
// SSE stream for elicitation events
5152
elicitation.get("/stream", async (c) => {
53+
const originHeader = c.req.header("origin");
54+
const { headers: corsHeaders } = buildCorsHeaders(originHeader, {
55+
allowCredentials: true,
56+
});
5257
const encoder = new TextEncoder();
5358
const stream = new ReadableStream<Uint8Array>({
5459
start(controller) {
@@ -85,11 +90,11 @@ elicitation.get("/stream", async (c) => {
8590
return new Response(stream as any, {
8691
status: 200,
8792
headers: {
93+
...corsHeaders,
8894
"Content-Type": "text/event-stream",
8995
"Cache-Control": "no-cache, no-transform",
9096
Connection: "keep-alive",
9197
"X-Accel-Buffering": "no",
92-
"Access-Control-Allow-Origin": "*",
9398
},
9499
});
95100
});

server/routes/mcp/http-adapters.ts

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Hono } from "hono";
22
import "../../types/hono";
33
import { handleJsonRpc, BridgeMode } from "../../services/mcp-http-bridge";
4+
import { buildCorsHeaders } from "../../utils/cors";
45

56
// In-memory SSE session store per serverId:sessionId
67
type Session = {
@@ -16,40 +17,49 @@ const latestSessionByServer: Map<string, string> = new Map();
1617
function createHttpHandler(mode: BridgeMode, routePrefix: string) {
1718
const router = new Hono();
1819

19-
router.options("/:serverId", (c) =>
20-
c.body(null, 204, {
21-
"Access-Control-Allow-Origin": "*",
22-
"Access-Control-Allow-Methods": "GET,POST,HEAD,OPTIONS",
23-
"Access-Control-Allow-Headers":
24-
"*, Authorization, Content-Type, Accept, Accept-Language",
25-
"Access-Control-Expose-Headers": "*",
26-
"Access-Control-Max-Age": "86400",
27-
}),
28-
);
20+
const handlePreflight = (c: any) => {
21+
const originHeader = c.req.header("origin");
22+
const { headers, allowedOrigin } = buildCorsHeaders(originHeader, {
23+
allowMethods: "GET,POST,HEAD,OPTIONS",
24+
allowHeaders:
25+
"Authorization, Content-Type, Accept, Accept-Language, X-MCPJam-Endpoint-Base",
26+
exposeHeaders: "*",
27+
maxAge: "86400",
28+
allowCredentials: true,
29+
allowPrivateNetwork: true,
30+
requestPrivateNetwork:
31+
c.req.header("access-control-request-private-network") === "true",
32+
});
33+
34+
if (originHeader && !allowedOrigin) {
35+
return c.json({ error: "Origin not allowed" }, 403, { Vary: "Origin" });
36+
}
37+
38+
headers.Vary = `${headers.Vary}, Access-Control-Request-Headers`;
39+
return c.body(null, 204, headers);
40+
};
41+
42+
router.options("/:serverId", handlePreflight);
2943

3044
// Wildcard variants to tolerate trailing paths (e.g., /mcp)
31-
router.options("/:serverId/*", (c) =>
32-
c.body(null, 204, {
33-
"Access-Control-Allow-Origin": "*",
34-
"Access-Control-Allow-Methods": "GET,POST,HEAD,OPTIONS",
35-
"Access-Control-Allow-Headers":
36-
"*, Authorization, Content-Type, Accept, Accept-Language",
37-
"Access-Control-Expose-Headers": "*",
38-
"Access-Control-Max-Age": "86400",
39-
}),
40-
);
45+
router.options("/:serverId/*", handlePreflight);
4146

4247
async function handleHttp(c: any) {
4348
const serverId = c.req.param("serverId");
4449
const method = c.req.method;
50+
const originHeader = c.req.header("origin");
51+
const { headers: corsHeaders } = buildCorsHeaders(originHeader, {
52+
exposeHeaders: "*",
53+
allowCredentials: true,
54+
});
4555

4656
// SSE endpoint for clients that probe/subscribe via GET; HEAD advertises event-stream
4757
if (method === "HEAD") {
4858
return c.body(null, 200, {
59+
...corsHeaders,
4960
"Content-Type": "text/event-stream",
5061
"Cache-Control": "no-cache",
5162
Connection: "keep-alive",
52-
"Access-Control-Allow-Origin": "*",
5363
"X-Accel-Buffering": "no",
5464
});
5565
}
@@ -127,18 +137,17 @@ function createHttpHandler(mode: BridgeMode, routePrefix: string) {
127137
},
128138
});
129139
return c.body(stream as any, 200, {
140+
...corsHeaders,
130141
"Content-Type": "text/event-stream",
131142
"Cache-Control": "no-cache",
132143
Connection: "keep-alive",
133-
"Access-Control-Allow-Origin": "*",
134-
"Access-Control-Expose-Headers": "*",
135144
"X-Accel-Buffering": "no",
136145
"Transfer-Encoding": "chunked",
137146
});
138147
}
139148

140149
if (method !== "POST") {
141-
return c.json({ error: "Unsupported request" }, 400);
150+
return c.json({ error: "Unsupported request" }, 400, corsHeaders);
142151
}
143152

144153
// Parse JSON body (best effort)
@@ -172,17 +181,21 @@ function createHttpHandler(mode: BridgeMode, routePrefix: string) {
172181
);
173182
if (!response) {
174183
// Notification → 202 Accepted
175-
return c.body("Accepted", 202, { "Access-Control-Allow-Origin": "*" });
184+
return c.body("Accepted", 202, corsHeaders);
176185
}
177186
return c.body(JSON.stringify(response), 200, {
187+
...corsHeaders,
178188
"Content-Type": "application/json",
179-
"Access-Control-Allow-Origin": "*",
180-
"Access-Control-Expose-Headers": "*",
181189
});
182190
}
183191

184192
// Endpoint to receive client messages for SSE transport: /:serverId/messages?sessionId=...
185193
router.post("/:serverId/messages", async (c) => {
194+
const originHeader = c.req.header("origin");
195+
const { headers: corsHeaders } = buildCorsHeaders(originHeader, {
196+
exposeHeaders: "*",
197+
allowCredentials: true,
198+
});
186199
const serverId = c.req.param("serverId");
187200
const url = new URL(c.req.url);
188201
const sessionId = url.searchParams.get("sessionId") || "";
@@ -195,7 +208,7 @@ function createHttpHandler(mode: BridgeMode, routePrefix: string) {
195208
}
196209
}
197210
if (!sess) {
198-
return c.json({ error: "Invalid session" }, 400);
211+
return c.json({ error: "Invalid session" }, 400, corsHeaders);
199212
}
200213
let body: any;
201214
try {
@@ -242,15 +255,9 @@ function createHttpHandler(mode: BridgeMode, routePrefix: string) {
242255
} catch {}
243256
}
244257
// 202 Accepted per SSE transport semantics
245-
return c.body("Accepted", 202, {
246-
"Access-Control-Allow-Origin": "*",
247-
"Access-Control-Expose-Headers": "*",
248-
});
258+
return c.body("Accepted", 202, corsHeaders);
249259
} catch (e: any) {
250-
return c.body("Error", 400, {
251-
"Access-Control-Allow-Origin": "*",
252-
"Access-Control-Expose-Headers": "*",
253-
});
260+
return c.body("Error", 400, corsHeaders);
254261
}
255262
});
256263

server/routes/mcp/servers.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { MCPServerConfig } from "@/sdk";
33
import "../../types/hono"; // Type extensions
44
import { rpcLogBus, type RpcLogEvent } from "../../services/rpc-log-bus";
55
import { logger } from "../../utils/logger";
6+
import { buildCorsHeaders } from "../../utils/cors";
67

78
const servers = new Hono();
89

@@ -48,7 +49,7 @@ servers.get("/status/:serverId", async (c) => {
4849
status,
4950
});
5051
} catch (error) {
51-
logger.error("Error getting server status", error, { serverId });
52+
logger.error("Error getting server status", error);
5253
return c.json(
5354
{
5455
success: false,
@@ -82,7 +83,7 @@ servers.get("/init-info/:serverId", async (c) => {
8283
initInfo,
8384
});
8485
} catch (error) {
85-
logger.error("Error getting initialization info", error, { serverId });
86+
logger.error("Error getting initialization info", error);
8687
return c.json(
8788
{
8889
success: false,
@@ -119,7 +120,7 @@ servers.delete("/:serverId", async (c) => {
119120
message: `Disconnected from server: ${serverId}`,
120121
});
121122
} catch (error) {
122-
logger.error("Error disconnecting server", error, { serverId });
123+
logger.error("Error disconnecting server", error);
123124
return c.json(
124125
{
125126
success: false,
@@ -190,7 +191,7 @@ servers.post("/reconnect", async (c) => {
190191
...(success ? {} : { error: message }),
191192
});
192193
} catch (error) {
193-
logger.error("Error reconnecting server", error, { serverId });
194+
logger.error("Error reconnecting server", error);
194195
return c.json(
195196
{
196197
success: false,
@@ -203,6 +204,11 @@ servers.post("/reconnect", async (c) => {
203204

204205
// Stream JSON-RPC messages over SSE for all servers.
205206
servers.get("/rpc/stream", async (c) => {
207+
const originHeader = c.req.header("origin");
208+
const { headers: corsHeaders } = buildCorsHeaders(originHeader, {
209+
exposeHeaders: "*",
210+
allowCredentials: true,
211+
});
206212
const serverIds = c.mcpClientManager.listServers();
207213
const url = new URL(c.req.url);
208214
const replay = parseInt(url.searchParams.get("replay") || "0", 10);
@@ -256,11 +262,10 @@ servers.get("/rpc/stream", async (c) => {
256262

257263
return new Response(stream, {
258264
headers: {
265+
...corsHeaders,
259266
"Content-Type": "text/event-stream",
260267
"Cache-Control": "no-cache",
261268
Connection: "keep-alive",
262-
"Access-Control-Allow-Origin": "*",
263-
"Access-Control-Expose-Headers": "*",
264269
},
265270
});
266271
});

0 commit comments

Comments
 (0)