You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(mcp): validate Host/Origin headers on MCP HTTP transports (#643)
## Motivation
RedisVL can expose a configured Redis index to MCP clients over HTTP,
through the `streamable-http` and `sse` transports. Until now, once such
a server was listening it would answer any HTTP request that reached its
port, without confirming that the request was actually addressed to it.
Validating that requests are intended for this server is a standard
piece of transport hardening, and one worth having on by default rather
than left to each deployment.
This change adds that check: the HTTP transports now confirm, on every
request, that it is genuinely addressed to this server before any tool
runs.
## What changed
The server validates the `Host` and `Origin` headers of each HTTP
request against allowlists and rejects anything that doesn't match,
before the request reaches the search or upsert tools. The protection is
on by default and needs no configuration for the common cases — the
allowlist is derived automatically from the address the server binds to,
so loopback and same-machine clients keep working exactly as before.
Requests that carry no `Origin` (typical of non-browser MCP clients) are
unaffected; only cross-site origins are scrutinized.
For deployments where the client-visible host legitimately differs from
the bind address — behind a reverse proxy, or bound to `0.0.0.0` and
reached by a public hostname — the allowlist can be extended, or the
check relaxed, through a new `server.transport_security` block in the
MCP config and matching `REDISVL_MCP_*` environment variables. This
mirrors the existing authentication configuration, so it should feel
familiar. Authentication and this check are complementary and
independent: auth decides *who* may call, while this decides *which
authority* a request may claim. The `stdio` transport has no network
surface and is untouched.
## Implementation notes
- A new `redisvl/mcp/transport_security.py` module holds a small ASGI
middleware plus the config resolution, with FastMCP/Starlette imports
deferred so the module stays importable without the optional `mcp`
extra.
- The guard is wired in through `RedisVLMCPServer.run_async`, so it
applies to both the CLI and any programmatic embedder, and runs
outermost — ahead of auth and the tool handlers.
- Defaults are fail-closed: the guard is enabled when unconfigured, and
a `0.0.0.0` bind trusts only loopback until the operator names its
public hosts.
## Tests & docs
Adds unit coverage for the middleware (host/origin matching, port and
IPv6 handling, case-insensitivity, enable/disable) and for its config
resolution, plus integration tests over real HTTP confirming that a
legitimate local client still succeeds while spoofed hosts and
cross-site origins are rejected. The MCP how-to and authentication
guides gain a "Transport Security" section covering the defaults and the
configuration knobs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes request handling on all HTTP MCP traffic (fail-closed by
default); misconfigured proxies or remote `0.0.0.0` deployments without
`allowed_hosts` can break legitimate clients until allowlists are set.
>
> **Overview**
> Adds **default-on Host/Origin validation** on `streamable-http` and
`sse` MCP transports to mitigate DNS rebinding and untrusted cross-site
browser calls, before JWT auth or tool handlers run.
>
> A new `redisvl/mcp/transport_security` module provides ASGI
`HostOriginValidationMiddleware`, bind-derived default hosts (loopback
for `127.0.0.1` / `0.0.0.0`), and config resolution (YAML
`server.transport_security` plus `REDISVL_MCP_*` env vars, env wins).
`RedisVLMCPServer.run_async` prepends this middleware outermost for HTTP
transports; `stdio` is unchanged.
>
> **Operator knobs:** extra `allowed_hosts` / `allowed_origins`,
`allow_any_origin`, or `enabled: false` for reverse-proxy setups. Docs
cover transport security separately from JWT auth and note that
`0.0.0.0` binds need explicit client-visible hosts in the allowlist.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
eab122d. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Streamable HTTP and SSE endpoints are **unauthenticated by default**. Binding to a non-loopback host without auth fails closed unless you pass `--allow-unauthenticated`; binding to loopback without auth only warns. For real deployments, enable JWT authentication (see {doc}`mcp_authentication`) rather than using `--allow-unauthenticated`. When not using `--read-only`, the `upsert-records` tool is also exposed to any client that can reach the server.
58
58
```
59
59
60
+
### Transport Security (Host / Origin Validation)
61
+
62
+
On the HTTP transports the server validates the request `Host` and `Origin` headers against allowlists before any tool runs. This is **on by default** and defends against [DNS rebinding](https://en.wikipedia.org/wiki/DNS_rebinding): without it, a malicious web page could rebind its own hostname to `127.0.0.1` and use the victim's browser to reach a local MCP server, even one bound to loopback.
63
+
64
+
-**Host:** the allowlist is derived automatically from the bind address. Loopback binds accept `localhost`, `127.0.0.1`, and `[::1]` (with or without the port), so local MCP clients work with no configuration.
65
+
-**Origin:** requests with no `Origin` header (typical of non-browser MCP clients) always pass. A request that carries a cross-site `Origin` is rejected unless that origin is explicitly allowlisted.
66
+
67
+
You only need to configure this for deployments where the client-visible `Host` differs from the bind address (a reverse proxy, or a `--host 0.0.0.0` bind reached via a public hostname). Use the `REDISVL_MCP_ALLOWED_HOSTS` / `REDISVL_MCP_ALLOWED_ORIGINS` env vars, or a `server.transport_security` block in the config:
68
+
69
+
```yaml
70
+
server:
71
+
redis_url: ${REDIS_URL}
72
+
transport_security:
73
+
allowed_hosts: [mcp.example.com] # extra Host values to accept
74
+
allowed_origins: [https://app.example.com] # browser origins to accept
# enabled: false # disable when a trusted proxy validates
77
+
```
78
+
79
+
```{note}
80
+
Behind a reverse proxy or gateway that already validates `Host`/`Origin`, set `server.transport_security.enabled: false` (or `allow_any_origin: true`) so the proxy's rewritten headers are not rejected.
81
+
```
82
+
60
83
Run it in read-only mode to expose search without upsert:
61
84
62
85
```bash
@@ -83,6 +106,10 @@ You can also control boot settings through environment variables:
83
106
|`REDISVL_MCP_READ_ONLY`| Disable `upsert-records` when set to `true`|
84
107
|`REDISVL_MCP_TOOL_SEARCH_DESCRIPTION`| Set the base search tool description text; RedisVL still appends schema-derived typed filter, `exists`, and `return_fields` hints |
85
108
|`REDISVL_MCP_TOOL_UPSERT_DESCRIPTION`| Override the upsert tool description |
109
+
|`REDISVL_MCP_ALLOWED_HOSTS`| Comma-separated extra `Host` header values to accept on HTTP transports (in addition to the bind-derived defaults) |
110
+
|`REDISVL_MCP_ALLOWED_ORIGINS`| Comma-separated browser `Origin` values to accept on HTTP transports |
111
+
|`REDISVL_MCP_ALLOW_ANY_ORIGIN`| Accept any `Origin` (for trusted-proxy setups) when set to `true`|
112
+
|`REDISVL_MCP_TRANSPORT_SECURITY_ENABLED`| Set to `false` to disable Host/Origin validation (e.g. behind a validating proxy) |
86
113
87
114
## Connect a Remote MCP Client
88
115
@@ -91,7 +118,7 @@ When using Streamable HTTP or SSE transport, point your MCP client at the server
91
118
-**Streamable HTTP**: `http://<host>:<port>/mcp`
92
119
-**SSE**: `http://<host>:<port>/sse`
93
120
94
-
> **Note:**`<host>` here is the bind address the server was started with. The default `127.0.0.1` only accepts connections from the same machine. To allow connections from other machines, start the server with `--host 0.0.0.0` and use the machine's actual IP or hostname in the client URL.
121
+
> **Note:**`<host>` here is the bind address the server was started with. The default `127.0.0.1` only accepts connections from the same machine. To allow connections from other machines, start the server with `--host 0.0.0.0` and use the machine's actual IP or hostname in the client URL. Because a `0.0.0.0` bind has no single canonical `Host`, add the client-visible hostname(s) to `REDISVL_MCP_ALLOWED_HOSTS` (or `server.transport_security.allowed_hosts`), or Host validation will reject those requests. See [Transport Security](#transport-security-host-origin-validation).
95
122
96
123
For example, to configure a remote MCP client to connect to a Streamable HTTP server running on `192.168.1.10:8000`:
0 commit comments