Skip to content

[SECURITY] Authentication bypass through Host header leading to RCE #1891

@litios

Description

@litios

Disclaimer

This vulnerability was reported 2 months ago (February 20th) to the mailing list in the Security.md file and as a Github Advisory in this repo. This was tested with version 2.0.1 back then, but I confirmed it still occurs with the current version (2.0.5). Affects all versions since 1.5.0.

The issue was ignored both in the email and in the GitHub Advisory. I let the developers know in the GitHub Advisory on March 30th that by April 20th, I would be posting this publicly since there seems to be no intention to fix it.

For those running it as a Docker image, the workaround is to verify you are not running the docker container image with the port binded to 0.0.0.0. If you run the image like this, you are affected:

docker run -p 6274:6274 mcpjam/mcp-inspector

Vulnerability analysis

Summary

An authorization bypass is possible through the Host header that allows an attacker to acquire a session token. This token can later be used to authenticate against /api/mcp/connect, triggering RCE.

This issue doesn't affect the HOSTED deployments.
This issue doesn't affect deployments through a Docker container whose port is bound only to localhost, since that works as expected. Bound to localhost is the current recommended approach, updated in the README now which was not when I reported this vulnerability

Details

The endpoint api/session-token is secured by only allowing access to localhost or the list of allowed hosts. The way the endpoint retrieves the information on who is reaching out is through the Host header:

app.get("/api/session-token", (c) => {
if (HOSTED_MODE) {
return strictModeResponse(c, "/api/session-token");
}
const host = c.req.header("Host");
if (!isAllowedHost(host, ALLOWED_HOSTS, HOSTED_MODE)) {
appLogger.warn(
`[Security] Token request denied - non-allowed Host: ${host}`,
);
return c.json(
{ error: "Token only available via localhost or allowed hosts" },
403,
);
}
return c.json({ token: getSessionToken() });
});

This can be bypassed by an attacker since they can override the provided Host header. This will return a valid token to the attacker.

Similarly, there is another way to acquire the token when production is enabled (this is the case for the Docker image and the public web page app.mcpjam.com):

// SECURITY: Only inject token for localhost or allowed hosts (in hosted mode)
// This prevents token leakage when bound to 0.0.0.0
const host = c.req.header("Host");

This allows an attacker to receive the token through a call to a non-existent endpoint.

With this token, they can achieve RCE through /api/mcp/connect in a similar way as in GHSA-232v-j27c-5pp6

PoC

I'm not providing the command for the RCE since is the same as in GHSA-232v-j27c-5pp6, but it can be seen in the images

curl http://IP:6274/api/session-token --header "Host: localhost"

Image Image

curl http://IP:6274/testest --header "Host: localhost"

Image

Impact

An unauthenticated remote attacker can acquire authentication privileges, giving them access to privileged action and therefore RCE through api/mcp/connect

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions