Skip to content

feat(mcp): validate Host/Origin headers on MCP HTTP transports#643

Merged
vishal-bala merged 1 commit into
mainfrom
feat/mcp-host-origin-validation
Jul 3, 2026
Merged

feat(mcp): validate Host/Origin headers on MCP HTTP transports#643
vishal-bala merged 1 commit into
mainfrom
feat/mcp-host-origin-validation

Conversation

@vishal-bala

@vishal-bala vishal-bala commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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


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.

Reviewed by Cursor Bugbot for commit eab122d. Bugbot is set up for automated code reviews on this repo. Configure here.

@vishal-bala vishal-bala added the auto:patch Increment the patch version when merged label Jul 2, 2026
@vishal-bala
vishal-bala requested a review from nkanu17 July 2, 2026 12:28
@jit-ci

jit-ci Bot commented Jul 2, 2026

Copy link
Copy Markdown

🛡️ Jit Security Scan Results

CRITICAL HIGH MEDIUM

✅ No security findings were detected in this PR


Security scan by Jit

Harden the MCP server's streamable-http and SSE transports by validating
the Host and Origin request headers against allowlists before any tool
call runs. The guard is on by default and auto-derives a safe allowlist
from the bind address, so loopback and local clients work with no config.

Non-loopback and reverse-proxy deployments can extend the allowlist (or
disable the guard) via a server.transport_security config block and
REDISVL_MCP_* environment variables, mirroring the existing auth pattern.
stdio has no network surface and is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vishal-bala
vishal-bala force-pushed the feat/mcp-host-origin-validation branch from 9d05390 to eab122d Compare July 2, 2026 13:29
@vishal-bala
vishal-bala marked this pull request as ready for review July 2, 2026 13:45
@vishal-bala
vishal-bala merged commit 826d12c into main Jul 3, 2026
57 checks passed
@vishal-bala
vishal-bala deleted the feat/mcp-host-origin-validation branch July 3, 2026 12:17
vishal-bala added a commit that referenced this pull request Jul 3, 2026
## Overview

Rolls up the **RedisVL MCP multi-index server** epic
([RAAE-1603](https://redislabs.atlassian.net/browse/RAAE-1603)) into
`main`. A single MCP server can now expose **one or more** configured
Redis index bindings — with discovery (`list-indexes`), explicit
per-call routing (an optional `index` argument on
`search-records`/`upsert-records`), and per-index write policy — while
existing **single-index configs and callers behave exactly as before**.

## This is a roll-up — every change was already reviewed and merged

This PR introduces **no new code**. It is the aggregate of a stacked
series of per-ticket PRs, each reviewed and squash-merged into the
`feature/raae-1603-mcp-multi-index` collector branch:

- [#629](#629) — RAAE-1604:
config + runtime refactor (immutable `BindingRuntime`, per-binding
startup/teardown, `resolve_binding` routing, single global concurrency
semaphore)
- [#630](#630) — RAAE-1605:
`list-indexes` discovery tool
- [#631](#631) — RAAE-1606:
`search-records` index routing
- [#632](#632) — RAAE-1607:
`upsert-records` index routing + per-index write policy
- [#633](#633) — RAAE-1608:
concept/how-to/README docs

Each stacked PR was reviewed (human + Cursor Bugbot) and its threads
resolved before merge; this branch is the sum of those merges plus a
sync with `main`.

## Synced with `main`

The branch is up to date with `main`, including the recently merged MCP
security work — **Host/Origin header validation
([#643](#643 and the
**nltk drop
([#645](#645 — pulled
in via merge, so this PR's diff contains **only** the epic's own changes
(main's commits are in the merge base).

## Security review

A security review of the epic diff was run (auth scope enforcement,
read-only/write policy, index routing, info leakage, injection,
config/vectorizer resolution). **No findings** — tokenless HTTP is
rejected upstream by the SDK auth middleware, scopes and read-only
policy are enforced at both registration and per-call, routing is
confined to configured bindings, and `redis_name` is never exposed by
`list-indexes`.

## Verification

- Full MCP unit + integration suite: **283 passed / 1 skipped** on
`redis:8.2` (now runs without nltk).
- `mypy` clean; `black`/`isort` clean.
- Backward compatibility covered by dedicated tests (single-binding
defaults, omitted-`index` behavior, unchanged response contract aside
from the additive `index` echo).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

[RAAE-1603]:
https://redislabs.atlassian.net/browse/RAAE-1603?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes MCP server startup, routing, and write boundaries across
bindings; single-index behavior is preserved but multi-index
misconfiguration or routing errors could affect production assistants.
> 
> **Overview**
> **Multi-index MCP server** — One process can expose several logical
index bindings from YAML `indexes`, each with its own schema inspection,
search config, vectorizer, and runtime limits. Startup is
all-or-nothing: any binding failure prevents the server from starting.
> 
> **Routing and discovery** — New **`list-indexes`** tool enumerates
logical ids, filterable fields, `upsert_available`, and optional limits
(without exposing `redis_name`). **`search-records`** and
**`upsert-records`** accept an optional **`index`** argument; responses
echo the resolved id. With a single binding, omitting `index` still
works; with multiple bindings, omitting or using an unknown id returns
`invalid_request`.
> 
> **Write policy** — Global `--read-only` / `REDISVL_MCP_READ_ONLY` plus
per-binding **`read_only: true`** combine into
**`effective_read_only`**. `upsert-records` is registered only if at
least one binding is writable; writes to a read-only binding fail with
**`forbidden`** before Redis is touched.
> 
> **Implementation** — Config drops the “exactly one index” rule and
adds **`description`** / **`read_only`** on bindings. Server state moves
to per-binding **`BindingRuntime`** and **`resolve_binding()`**; removed
single-index helpers like `get_index()`. Search tool descriptions omit
per-schema hints when multiple indexes are configured and point clients
to `list-indexes`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
553e55f. 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 (1M context) <noreply@anthropic.com>
@applied-ai-release-bot

Copy link
Copy Markdown

🚀 PR was released in v0.23.0 🚀

@applied-ai-release-bot applied-ai-release-bot Bot added the released This issue/pull request has been released. label Jul 3, 2026
ahsd-coder pushed a commit to ahsd-coder/LLM-Smart-Cache that referenced this pull request Jul 7, 2026
## Overview

Rolls up the **RedisVL MCP multi-index server** epic
([RAAE-1603](https://redislabs.atlassian.net/browse/RAAE-1603)) into
`main`. A single MCP server can now expose **one or more** configured
Redis index bindings — with discovery (`list-indexes`), explicit
per-call routing (an optional `index` argument on
`search-records`/`upsert-records`), and per-index write policy — while
existing **single-index configs and callers behave exactly as before**.

## This is a roll-up — every change was already reviewed and merged

This PR introduces **no new code**. It is the aggregate of a stacked
series of per-ticket PRs, each reviewed and squash-merged into the
`feature/raae-1603-mcp-multi-index` collector branch:

- [#629](redis/redis-vl-python#629) — RAAE-1604:
config + runtime refactor (immutable `BindingRuntime`, per-binding
startup/teardown, `resolve_binding` routing, single global concurrency
semaphore)
- [#630](redis/redis-vl-python#630) — RAAE-1605:
`list-indexes` discovery tool
- [#631](redis/redis-vl-python#631) — RAAE-1606:
`search-records` index routing
- [#632](redis/redis-vl-python#632) — RAAE-1607:
`upsert-records` index routing + per-index write policy
- [#633](redis/redis-vl-python#633) — RAAE-1608:
concept/how-to/README docs

Each stacked PR was reviewed (human + Cursor Bugbot) and its threads
resolved before merge; this branch is the sum of those merges plus a
sync with `main`.

## Synced with `main`

The branch is up to date with `main`, including the recently merged MCP
security work — **Host/Origin header validation
([#643](redis/redis-vl-python#643 and the
**nltk drop
([#645](redis/redis-vl-python#645 — pulled
in via merge, so this PR's diff contains **only** the epic's own changes
(main's commits are in the merge base).

## Security review

A security review of the epic diff was run (auth scope enforcement,
read-only/write policy, index routing, info leakage, injection,
config/vectorizer resolution). **No findings** — tokenless HTTP is
rejected upstream by the SDK auth middleware, scopes and read-only
policy are enforced at both registration and per-call, routing is
confined to configured bindings, and `redis_name` is never exposed by
`list-indexes`.

## Verification

- Full MCP unit + integration suite: **283 passed / 1 skipped** on
`redis:8.2` (now runs without nltk).
- `mypy` clean; `black`/`isort` clean.
- Backward compatibility covered by dedicated tests (single-binding
defaults, omitted-`index` behavior, unchanged response contract aside
from the additive `index` echo).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

[RAAE-1603]:
https://redislabs.atlassian.net/browse/RAAE-1603?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes MCP server startup, routing, and write boundaries across
bindings; single-index behavior is preserved but multi-index
misconfiguration or routing errors could affect production assistants.
> 
> **Overview**
> **Multi-index MCP server** — One process can expose several logical
index bindings from YAML `indexes`, each with its own schema inspection,
search config, vectorizer, and runtime limits. Startup is
all-or-nothing: any binding failure prevents the server from starting.
> 
> **Routing and discovery** — New **`list-indexes`** tool enumerates
logical ids, filterable fields, `upsert_available`, and optional limits
(without exposing `redis_name`). **`search-records`** and
**`upsert-records`** accept an optional **`index`** argument; responses
echo the resolved id. With a single binding, omitting `index` still
works; with multiple bindings, omitting or using an unknown id returns
`invalid_request`.
> 
> **Write policy** — Global `--read-only` / `REDISVL_MCP_READ_ONLY` plus
per-binding **`read_only: true`** combine into
**`effective_read_only`**. `upsert-records` is registered only if at
least one binding is writable; writes to a read-only binding fail with
**`forbidden`** before Redis is touched.
> 
> **Implementation** — Config drops the “exactly one index” rule and
adds **`description`** / **`read_only`** on bindings. Server state moves
to per-binding **`BindingRuntime`** and **`resolve_binding()`**; removed
single-index helpers like `get_index()`. Search tool descriptions omit
per-schema hints when multiple indexes are configured and point clients
to `list-indexes`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
553e55f4e93b306b643e3b8aca157e7a40a1497b. 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 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto:patch Increment the patch version when merged released This issue/pull request has been released.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant