Skip to content

fix(mcp): reject origins outside the configured allowlist - #42649

Merged
joshua-berri merged 1 commit into
mainfrom
litellm_fix_mcp_origin_validation
Sep 24, 2026
Merged

joshua-berri merged 1 commit into
mainfrom
litellm_fix_mcp_origin_validation

Conversation

@joshua-berri

@joshua-berri joshua-berri commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • MCP requests bypass configured Origin restrictions

How it solves it:

  • Reject disallowed Origins before authentication and upstream execution
  • Reuse the existing CORS allowlist for HTTP and SSE

User Flow

Before: configured Origin restrictions only control browser response visibility

  1. The operator sets LITELLM_CORS_ORIGINS=http://localhost:4135 and starts the gateway
  2. A client sends an authenticated initialize request to http://localhost:4135/mcp/conformance_ref with Origin: https://untrusted.example
  3. The gateway processes the request and returns HTTP 200

After: configured Origin restrictions also reject MCP requests

  1. The operator sets LITELLM_CORS_ORIGINS=http://localhost:4135 and starts the gateway
  2. A client sends an authenticated initialize request to http://localhost:4135/mcp/conformance_ref with Origin: https://untrusted.example
  3. The gateway returns HTTP 403 before authentication or upstream execution

Relevant issues

The MCP transport specification requires HTTP 403 for a disallowed Origin

Affected release

Reproduced on v1.102.0 and main; introduction version has not been established

Linear ticket

Prerequisite for LIT-7744. The CI-only conformance gate will ship separately

Pre-Submission checklist

  • I have added meaningful tests
  • The affected backend test file passes locally
  • My PR passes all required CI/CD checks
  • My PR only solves one specific problem
  • Greptile confidence is at least 4/5

Screenshots / Proof of Fix

Source-built verification after the rebase onto main, using a DB-free gateway and the official MCP reference server (@modelcontextprotocol/server-everything 2026.8.31, streamable HTTP on port 3301). Baseline listened on 4137, tip on 4135; each process had LITELLM_CORS_ORIGINS=http://localhost:<its port>. Both imported their own worktree source with the same locked environment

Shared setup: configure an HTTP MCP server named conformance_ref pointing to http://localhost:3301/mcp and set KEY to the gateway master key. Request bodies:

INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"origin-probe","version":"1.0.0"}}}'
CALL='{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"message":"MARKER_UPSTREAM_CALL"}}}'

Before (093ceb5)

Streamable HTTP initialize

  1. Send an authenticated initialize with a disallowed Origin:

    curl -s -D - -X POST http://localhost:4137/mcp/conformance_ref \
      -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -H 'Origin: http://evil.example.com' --data "$INIT"

    Observed HTTP/1.1 200 OK and an initialized MCP session

  2. Repeat with Origin: http://localhost:4137, with no Origin, with duplicate Origin headers (allowed plus evil in both orders), with Origin: null, and against the root POST /mcp: all return 200

  3. Repeat the evil-Origin initialize without the Authorization header: 401 {"error":{"message":"Authentication required"...}}

Streamable HTTP tool execution

  1. Send $CALL with the allowed Origin and with no Origin: both return 200 with Echo: MARKER_UPSTREAM_CALL
  2. Send $CALL with Origin: http://evil.example.com, then again with mcp-session-id: bogus: both return 200 and the echo text
  3. Count MARKER_UPSTREAM_CALL in the gateway debug log: it grows from 8 to 16 across the two denied-Origin calls, so both executed upstream
  4. GET /mcp/conformance_ref and DELETE /mcp/conformance_ref with the evil Origin and a bogus session return 200; DELETE with the allowed Origin and no session returns 405

Legacy SSE

  1. GET http://localhost:4137/mcp/sse with Origin: http://evil.example.com: HTTP/1.1 200 OK event stream. Same with duplicate allowed plus evil headers, the allowed Origin, and no Origin
  2. The same evil-Origin GET without Authorization: 401 Unauthorized
  3. POST http://localhost:4137/mcp/sse/messages with $INIT and the evil Origin: 400 session_id is required, the same protocol response as the allowed and no-Origin cases

After (1321eda)

Streamable HTTP initialize

  1. Send the same initialize against the tip:

    curl -s -D - -X POST http://localhost:4135/mcp/conformance_ref \
      -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -H 'Origin: http://evil.example.com' --data "$INIT"

    Observed HTTP/1.1 403 Forbidden and {"detail":"Invalid Origin header"}

  2. Origin: http://localhost:4135 and no Origin still return 200 with the initialized session. Duplicate headers in either order, Origin: null, and the root POST /mcp with the evil Origin return 403

  3. The evil-Origin initialize without Authorization returns 403 Invalid Origin header, so the Origin check runs before authentication

Streamable HTTP tool execution

  1. $CALL with the allowed Origin and with no Origin: 200 with Echo: MARKER_UPSTREAM_CALL, unchanged
  2. $CALL with the evil Origin, with and without mcp-session-id: bogus: 403 {"detail":"Invalid Origin header"}
  3. MARKER_UPSTREAM_CALL stays at 8 in the gateway debug log across the two denied calls: zero denied upstream executions
  4. GET and DELETE /mcp/conformance_ref with the evil Origin return 403; DELETE with the allowed Origin and no session still returns 405

Legacy SSE

  1. GET http://localhost:4135/mcp/sse with the evil Origin or duplicate allowed plus evil headers: HTTP/1.1 403 Forbidden. The allowed Origin and no Origin still return the 200 event stream
  2. The evil-Origin GET without Authorization: 403 Forbidden
  3. POST http://localhost:4135/mcp/sse/messages with the evil Origin: 403 {"detail":"Invalid Origin header"}; allowed and no-Origin requests still return 400 session_id is required

A third run of the tip on port 4136 with LITELLM_CORS_ORIGINS unset (default wildcard policy) reproduced every Before result exactly, including 16 upstream markers, so default and wildcard deployments are unchanged

Local gates at the tip: make lint passed every gate (Ruff lint and format, strict-rule, type-discipline, test-quality, basedpyright, circular imports). The mapped test file passed 389 tests with --cov-branch: all 6 changed executable lines and both outcomes of the new guard condition are covered. test_wrapper_async_does_not_fire_failure_hook_for_post_success_error, which failed in misc / Run tests at the previous merge base, was changed on main in #42646 and passes at this tip

Type

Bug Fix

Caveats

Severe

  • Explicit Origin restrictions now reject MCP requests server-side

Low

  • Existing default and wildcard policies remain permissive
  • Clients without Origin continue through normal authentication
  • No new Host validation policy or SDK upgrade

Final Attestation

  • Tests and independent verification cover the required behavior

Link to Devin session: https://app.devin.ai/sessions/f91a8ec1cc4b40d09f76e408c127c23b
Open in Devin Desktop: https://app.devin.ai/desktop/session/f91a8ec1cc4b40d09f76e408c127c23b?variant=devin


Note

Medium Risk
Tightens MCP admission when explicit CORS origins are set; wildcard/default behavior is unchanged, but operators with a fixed allowlist may see previously accepted cross-origin MCP clients start failing with 403.

Overview
MCP Streamable HTTP and legacy SSE entry points now enforce the same Origin policy as the gateway CORS allowlist (LITELLM_CORS_ORIGINS / origins), instead of only affecting browser-visible responses.

A new reject_disallowed_mcp_origin runs at the start of both handlers. When the allowlist is not wildcard (*), any Origin value not in the list yields HTTP 403 with Invalid Origin header, including duplicate headers, null, empty, and subdomain lookalikes. Requests with no Origin still proceed to normal auth. Wildcard CORS config is unchanged.

The check runs before MCP authentication and upstream work, so disallowed origins no longer initialize sessions or execute tools. Tests cover multiple MCP routes/methods and assert auth is skipped on 403.

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

@joshua-berri
joshua-berri requested a review from a team September 23, 2026 02:04
@joshua-berri

Copy link
Copy Markdown
Contributor Author

@greptileai Please review 7f2dcea for Origin rejection, compatibility, and denied upstream execution across both MCP transports.

bugbot run

@greptile-apps

greptile-apps Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with both MCP transports consistently enforcing the configured policy before authentication or upstream work

Summary

This PR adds MCP transport security hardening by applying the configured CORS allowlist before authentication and upstream execution

  • Enforces exact Origin matching for streamable HTTP and legacy SSE requests
  • Preserves requests without an Origin and deployments using the wildcard policy
  • Adds regression coverage across both transports, multiple methods, duplicate headers, and authentication ordering

Reviews (2) · Last reviewed commit: "fix(mcp): reject origins outside the con..."

@joshua-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

Please review commit 7f2dcea for Origin validation, preserved client compatibility, and blocked upstream execution.

@codspeed

codspeed Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_fix_mcp_origin_validation (1321eda) with main (093ceb5)

Open in CodSpeed

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@codecov

codecov Bot commented Sep 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@devin-ai-integration
devin-ai-integration Bot force-pushed the litellm_fix_mcp_origin_validation branch from 7f2dcea to 1321eda Compare September 24, 2026 06:20
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai please review 1321eda after the rebase onto main for Origin rejection across both MCP transports

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 1321eda. Configure here.

@joshua-berri
joshua-berri merged commit 57eb3ff into main Sep 24, 2026
112 checks passed
@joshua-berri
joshua-berri deleted the litellm_fix_mcp_origin_validation branch September 24, 2026 17:23
devin-ai-integration Bot pushed a commit that referenced this pull request Sep 24, 2026
Co-authored-by: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
e2e-changed — 1321eda1 Deployed Sep 24, 2026 by devin-ai-integration[bot] via oauth #747
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants