Topic: mcp server request smuggling security

MCP server request smuggling security — HTTP desync via proxy misparse

HTTP request smuggling occurs when a front-end proxy and a back-end server parse the boundary between two HTTP/1.1 requests differently, causing a single connection's byte stream to be split into requests in conflicting ways. For MCP servers deployed with HTTP transport behind a reverse proxy — Nginx, Caddy, AWS ALB, Cloudflare — this can let an attacker prefix arbitrary content to the next legitimate request, poisoning responses, hijacking sessions, or bypassing authentication checks that the proxy performs but the backend does not.

Why MCP servers with HTTP transport are exposed

MCP's HTTP transport sends JSON-RPC messages as POST requests to a single endpoint. In a production deployment, the MCP server runs on an internal port and receives traffic through a reverse proxy that handles TLS termination, authentication headers, and rate limiting. This two-hop architecture — client → proxy → MCP server — is exactly the configuration that creates a request smuggling surface. The proxy parses the HTTP layer and forwards the body; the backend (your Node.js/Python MCP server) parses the HTTP layer again. If they disagree on where one request ends and the next begins, the attacker controls what the backend sees as the "next" request.

Two desync patterns account for nearly all practical HTTP/1.1 request smuggling attacks:

CL.TE desync: front-end uses Content-Length, back-end uses Transfer-Encoding

The attacker sends a request with both a Content-Length header and a Transfer-Encoding: chunked header. RFC 7230 says Transfer-Encoding takes priority, but many reverse proxies (particularly older Nginx configs and AWS Classic Load Balancers) use Content-Length to forward a fixed number of bytes to the backend while the backend uses chunked encoding to determine message end. The attacker crafts the Content-Length to include a small prefix of a second request in the "body" of the first:

POST /mcp HTTP/1.1
Host: skillaudit.dev
Content-Length: 58
Transfer-Encoding: chunked

0

POST /admin/internal HTTP/1.1
Host: skillaudit.dev
X-Forwarded-For: 127.0.0.1

The proxy reads 58 bytes as the Content-Length body and forwards the chunk terminator plus the poisoned prefix. The backend, parsing chunked encoding, sees the 0\r\n\r\n as the end of the first request — and interprets the smuggled POST /admin/internal lines as the beginning of the next request on the same connection. The next legitimate client request gets the attacker's prefix prepended, potentially bypassing the proxy's authentication check or hitting an internal endpoint the proxy wouldn't route to.

TE.CL desync: front-end uses Transfer-Encoding, back-end uses Content-Length

The reverse variant: the proxy parses chunked encoding while the backend parses Content-Length. The attacker sends a chunked request where the declared chunk sizes do not match the actual byte boundaries of the content, causing the backend to read more or fewer bytes than the proxy forwarded as a single request. This is rarer in practice but can be triggered in servers that implement their own HTTP parsing (a common pattern in Python MCP servers using raw ASGI without a full HTTP library).

HTTP/2 downgrade smuggling

A subtler variant affects MCP servers deployed behind a proxy that accepts HTTP/2 from clients but translates to HTTP/1.1 internally. HTTP/2 is immune to CL.TE and TE.CL attacks because it uses binary framing instead of header-delimited length. But if the proxy converts HTTP/2 requests to HTTP/1.1 for the backend and the conversion introduces a Content-Length mismatch with the actual body length, the backend may be vulnerable to a version-desync attack. This is particularly relevant for Nginx's proxy_http_version 1.1 configuration with MCP servers that don't run a full HTTP/2 stack internally.

Mitigations

Use HTTP/2 end-to-end. If your reverse proxy supports HTTP/2 to the backend (h2c on internal connections), request smuggling via CL.TE desync is eliminated at the transport level. HTTP/2 binary framing defines message boundaries unambiguously.

Reject requests with both Content-Length and Transfer-Encoding. In your MCP server's HTTP handler, if a request arrives with both headers present, return 400 Bad Request rather than attempting to interpret priority. In Express:

app.use((req, res, next) => {
  if (req.headers['content-length'] && req.headers['transfer-encoding']) {
    return res.status(400).json({ error: 'Ambiguous content framing' });
  }
  next();
});

Normalize connection reuse policy. Disable HTTP/1.1 keep-alive on the proxy-to-backend connection if request smuggling is a concern in your threat model. Each request on a fresh connection eliminates the mechanism by which a smuggled prefix can poison the next request.

Validate host headers. The most impactful smuggling attacks route to internal endpoints the attacker can't reach directly. If your MCP server validates the Host header and rejects requests for hostnames other than its own, the attacker loses the ability to route the smuggled prefix to privileged internal paths.

SkillAudit's security axis flags MCP servers running HTTP transport without end-to-end HTTP/2 or Content-Length/Transfer-Encoding conflict rejection as a medium-severity finding, particularly when the server is deployed behind a proxy.

Audit your MCP server's HTTP transport configuration for desync vulnerabilities.

Run a free audit →