mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 02:32:54 +00:00
fix(mcp): preserve repeated HTTP host allowlists (#8164)
This commit is contained in:
1 parent
47fa6ab379
commit
d48e472fa6
4 files changed
+148
-4
No files matched your search
@@ -29,7 +29,7 @@ const MCP_HTTP_OPTIONS = z.object({
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"Host/Origin names a non-loopback bind may serve; default MCP_ALLOWED_HOSTS, else the bind is refused",
|
||||
"Host/Origin names a non-loopback bind may serve; repeat or separate names with commas; default MCP_ALLOWED_HOSTS, else the bind is refused",
|
||||
),
|
||||
help: z.boolean().optional().describe("Show help"),
|
||||
});
|
||||
@@ -85,6 +85,7 @@ export const parseMcpHttpArguments = (
|
||||
arguments_: readonly string[],
|
||||
): McpHttpArguments => {
|
||||
const values = new Map<string, string>();
|
||||
const allowedHostValues: string[] = [];
|
||||
let http = false;
|
||||
for (let index = 0; index < arguments_.length; index += 1) {
|
||||
const argument = arguments_[index] ?? "";
|
||||
@@ -100,7 +101,8 @@ export const parseMcpHttpArguments = (
|
||||
if (inline === undefined) index += 1;
|
||||
if (value === undefined || (inline === undefined && value.startsWith("-")))
|
||||
return invalid(`Missing value for ${flag}`);
|
||||
values.set(flag, value);
|
||||
if (flag === "--allowed-hosts") allowedHostValues.push(value);
|
||||
else values.set(flag, value);
|
||||
}
|
||||
if (!http) return invalid("--host and --port require --http");
|
||||
const host = values.get("--host")?.trim();
|
||||
@@ -111,7 +113,8 @@ export const parseMcpHttpArguments = (
|
||||
portText === undefined ? undefined : mcpHttpPortSchema.safeParse(portText);
|
||||
if (port !== undefined && !port.success)
|
||||
return invalid(`Invalid --port: ${MCP_HTTP_PORT_CONSTRAINT}`);
|
||||
const allowedHostsText = values.get("--allowed-hosts");
|
||||
const allowedHostsText =
|
||||
allowedHostValues.length === 0 ? undefined : allowedHostValues.join(",");
|
||||
const allowedHosts =
|
||||
allowedHostsText === undefined
|
||||
? undefined
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from "./serveStreamableHttp.js";
|
||||
import { StreamableHttpListenError } from "./streamableHttpListenError.js";
|
||||
import { mcpAllowedHostsTextSchema } from "../../config/mcpHttpEndpoint.js";
|
||||
import { parseMcpHttpArguments } from "../../main/mcpHttpArguments.js";
|
||||
import { RequestTrackingMcpServer } from "../RequestTrackingMcpServer.js";
|
||||
|
||||
const servers: StreamableHttpServer[] = [];
|
||||
@@ -346,6 +347,49 @@ it.each([
|
||||
expect(JSON.parse(response.body)).toEqual({ allowed: true });
|
||||
});
|
||||
|
||||
it("serves every declared repeated host while refusing undeclared hosts and origins", async () => {
|
||||
const parsed = parseMcpHttpArguments([
|
||||
"--http",
|
||||
"--allowed-hosts",
|
||||
"a.example",
|
||||
"--allowed-hosts=b.example",
|
||||
]);
|
||||
if (!parsed.ok || parsed.allowedHosts === undefined)
|
||||
throw new Error("Expected valid repeated host lists");
|
||||
const { served } = await start({
|
||||
host: "0.0.0.0",
|
||||
allowedHosts: parsed.allowedHosts,
|
||||
routes: [
|
||||
{
|
||||
method: "GET",
|
||||
path: "/probe",
|
||||
handle: () => Response.json({ allowed: true }),
|
||||
},
|
||||
],
|
||||
});
|
||||
const url = new URL(served.url);
|
||||
url.hostname = "127.0.0.1";
|
||||
url.pathname = "/probe";
|
||||
for (const host of ["a.example", "b.example"]) {
|
||||
const response = await rawHttpRequest(url, {
|
||||
headers: { host, origin: `http://${host}` },
|
||||
});
|
||||
expect(response.status, host).toBe(200);
|
||||
expect(JSON.parse(response.body)).toEqual({ allowed: true });
|
||||
}
|
||||
expect(
|
||||
(await rawHttpRequest(url, { headers: { host: "undeclared.example" } }))
|
||||
.status,
|
||||
).toBe(403);
|
||||
expect(
|
||||
(
|
||||
await rawHttpRequest(url, {
|
||||
headers: { host: "a.example", origin: "http://undeclared.example" },
|
||||
})
|
||||
).status,
|
||||
).toBe(403);
|
||||
});
|
||||
|
||||
it("announces an admitted non-loopback URL and gates it on the operator allowlist", async () => {
|
||||
const { served } = await start({
|
||||
host: "0.0.0.0",
|
||||
|
||||
Reference in new issue
Block a user