Summary
The Express-based HTTP transport exposes an MCP endpoint that does not validate the Origin or Host header of incoming requests. This violates the single mandatory security requirement the MCP specification places on HTTP transports (Basic/Transports, "Security Warning"):
- Servers MUST validate the
Origin header on all incoming connections to prevent DNS rebinding attacks
- When running locally, servers SHOULD bind only to localhost (127.0.0.1) rather than all network interfaces (0.0.0.0)
- Servers SHOULD implement proper authentication for all connections
Without these protections, attackers could use DNS rebinding to interact with local MCP servers from remote websites.
The SDK's StreamableHTTPServerTransport ships the controls to satisfy requirement 1 (enableDnsRebindingProtection, allowedHosts, allowedOrigins); none are configured. As a result any web page the victim visits can issue cross-origin JSON-RPC requests to the victim's MCP endpoint and invoke any registered tool.
Affected code
src/transport/http.ts:
app.post('/mcp', async (req, res) => {
const server = createServer();
registerAll(server);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // no allowedHosts / allowedOrigins /
enableJsonResponse: true // enableDnsRebindingProtection configured
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
The Origin and Host headers are never inspected, by the transport or by the surrounding Express app, so a browser-originated cross-origin request is accepted exactly like a legitimate local client request.
Impact
- A malicious website open in the victim's browser can send JSON-RPC calls to
http://127.0.0.1:<port>/mcp and invoke any registered tool (classic CSRF against a local service; DNS rebinding defeats same-origin over time even where the browser would otherwise block reads).
- Combined with advisory GHSA-38f8-m897-jm7w, the attacker-controlled page can call
sparql_query with an endpoint_url that redirects or rebinds to internal or cloud-metadata addresses — turning a drive-by page visit into internal network access and potential credential disclosure from the victim's host.
CKAN_ALLOWED_DOMAINS restricts which CKAN hosts may be queried on the CKAN path, but is not an origin control: it does not constrain who may connect to the MCP endpoint, and does not apply to sparql_query.
Severity note: rated Medium (AC:H / UI:R) because exploitation depends on the victim running the HTTP transport and visiting an attacker page.
Deployment context
Two properties of the shipped configuration remove the practical barriers to reaching the unvalidated endpoint. Both correspond to the specification's SHOULD-level recommendations (2 and 3 above) rather than to its MUST-level requirement; neither is an independent defect — with Origin/Host validation in place, neither is exploitable by a remote page — but both widen who can attempt it:
-
No authentication on /mcp. The endpoint requires no credential and advertises authorization_not_supported at /.well-known/oauth-authorization-server. This is the expected shape for a local MCP server, which is why the specification recommends authentication (SHOULD) while mandating origin validation (MUST).
-
Bind address. app.listen(port, cb) binds 0.0.0.0/::, and the repository's docker-compose.yml publishes 3000:3000 on all host interfaces with TRANSPORT: http. Its healthcheck confirms the endpoint answers unauthenticated JSON-RPC:
curl -sf -X POST http://localhost:3000/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
On such a deployment the endpoint is reachable from the local network segment, so the UI:R precondition above does not apply to a same-LAN attacker.
Proof of concept
poc/verify-config.mjs — static confirmation that no origin/host validation is configured in the shipped source.
poc/dns-rebinding-poc.html — a web page that issues cross-origin initialize / tools/list / tools/call requests to http://127.0.0.1:3000/mcp.
$ node poc/verify-config.mjs src/transport/http.ts
[x] No DNS-rebinding protection (allowedHosts)
[x] No origin allowlist (allowedOrigins)
[x] No enableDnsRebindingProtection flag
Remediation
Validate the request origin on /mcp — one control, provided by the transport:
new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
enableDnsRebindingProtection: true,
allowedHosts: ['127.0.0.1:3000', 'localhost:3000'],
allowedOrigins: ['http://127.0.0.1:3000', 'http://localhost:3000'],
});
Recommended alongside, as deployment hardening rather than as a fix for this issue: bind to loopback by default (app.listen(port, '127.0.0.1', ...)), publish the container port as 127.0.0.1:3000:3000, and front any network-exposed deployment with an authenticating reverse proxy.
Summary
The Express-based HTTP transport exposes an MCP endpoint that does not validate the
OriginorHostheader of incoming requests. This violates the single mandatory security requirement the MCP specification places on HTTP transports (Basic/Transports, "Security Warning"):The SDK's
StreamableHTTPServerTransportships the controls to satisfy requirement 1 (enableDnsRebindingProtection,allowedHosts,allowedOrigins); none are configured. As a result any web page the victim visits can issue cross-origin JSON-RPC requests to the victim's MCP endpoint and invoke any registered tool.Affected code
src/transport/http.ts:The
OriginandHostheaders are never inspected, by the transport or by the surrounding Express app, so a browser-originated cross-origin request is accepted exactly like a legitimate local client request.Impact
http://127.0.0.1:<port>/mcpand invoke any registered tool (classic CSRF against a local service; DNS rebinding defeats same-origin over time even where the browser would otherwise block reads).sparql_querywith anendpoint_urlthat redirects or rebinds to internal or cloud-metadata addresses — turning a drive-by page visit into internal network access and potential credential disclosure from the victim's host.CKAN_ALLOWED_DOMAINSrestricts which CKAN hosts may be queried on the CKAN path, but is not an origin control: it does not constrain who may connect to the MCP endpoint, and does not apply tosparql_query.Severity note: rated Medium (AC:H / UI:R) because exploitation depends on the victim running the HTTP transport and visiting an attacker page.
Deployment context
Two properties of the shipped configuration remove the practical barriers to reaching the unvalidated endpoint. Both correspond to the specification's SHOULD-level recommendations (2 and 3 above) rather than to its MUST-level requirement; neither is an independent defect — with
Origin/Hostvalidation in place, neither is exploitable by a remote page — but both widen who can attempt it:No authentication on
/mcp. The endpoint requires no credential and advertisesauthorization_not_supportedat/.well-known/oauth-authorization-server. This is the expected shape for a local MCP server, which is why the specification recommends authentication (SHOULD) while mandating origin validation (MUST).Bind address.
app.listen(port, cb)binds0.0.0.0/::, and the repository'sdocker-compose.ymlpublishes3000:3000on all host interfaces withTRANSPORT: http. Its healthcheck confirms the endpoint answers unauthenticated JSON-RPC:On such a deployment the endpoint is reachable from the local network segment, so the
UI:Rprecondition above does not apply to a same-LAN attacker.Proof of concept
poc/verify-config.mjs— static confirmation that no origin/host validation is configured in the shipped source.poc/dns-rebinding-poc.html— a web page that issues cross-origininitialize/tools/list/tools/callrequests tohttp://127.0.0.1:3000/mcp.Remediation
Validate the request origin on
/mcp— one control, provided by the transport:Recommended alongside, as deployment hardening rather than as a fix for this issue: bind to loopback by default (
app.listen(port, '127.0.0.1', ...)), publish the container port as127.0.0.1:3000:3000, and front any network-exposed deployment with an authenticating reverse proxy.