# `ConduitMcp.Plugs.OriginValidation`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/plugs/origin_validation.ex#L1)

Plug that validates the `Origin` request header against an allowlist.

Reads the allowlist from `conn.private[:allowed_origins]`. Accepted shapes:

| `:allowed_origins` | Behaviour |
|---|---|
| `nil` (unset) | **Fails closed**: any request carrying an `Origin` is rejected |
| `"*"` | All origins allowed — the explicit opt-out |
| a list of strings | Only those origins are allowed (exact match) |
| a bare string | Only that origin is allowed (exact match) |
| a `Regex` | Origins matching the pattern are allowed |

`ConduitMcp.Transport.StreamableHTTP` and `ConduitMcp.Transport.SSE` check
the shape at `init/1` and raise `ArgumentError` for anything else. When this
plug is used on its own, an unsupported value fails closed.

> #### Anchor a `Regex` allowlist {: .warning}
>
> The pattern is tested with `Regex.match?/2`, which matches anywhere in the
> origin. `~r/https:\/\/example\.com/` therefore also allows
> `https://example.com.evil.test`. Anchor both ends:
> `~r/\Ahttps:\/\/example\.com\z/`.

Other rules:

- OPTIONS requests always pass (CORS preflight; the router answers them
  before any MCP handler runs)
- Requests **without** an `Origin` header pass — see below
- Disallowed origins receive a 403 JSON error response

## Why an unset allowlist fails closed

A warning does not stop a request: a page on `https://evil.example` could
POST to a loopback MCP server and, if the response carried
`access-control-allow-origin: *`, read the reply. "No browser origin is
trusted" is the only default that is safe for a server bound to loopback on
a developer machine.

Pass `allowed_origins: "*"` to allow every origin explicitly.

## Why missing `Origin` still passes — and what that does not cover

Native MCP clients (Claude Desktop, IDEs, CLIs) are not browsers and do not
send an `Origin` header. Rejecting header-less requests would break every
legitimate non-browser client, so they pass.

> #### Origin validation is not DNS-rebinding protection {: .warning}
>
> After a successful DNS rebind the attacker's page is, from the browser's
> point of view, *same-origin* with your server — and browsers attach no
> `Origin` to a same-origin `GET`. Such a request therefore takes the
> header-less path above and this plug never runs its allowlist. What Origin
> validation *does* cover is the cross-origin case: a page on another origin
> cannot POST to your server, because a cross-origin POST always carries
> `Origin`.
>
> The control that covers rebinding is `Host` validation, which this library
> does not implement. For a server a browser could reach — anything bound to
> loopback on a developer machine — put it behind a proxy that rejects
> unexpected `Host` values, or require authentication so the rebound request
> has no credential. This matters most for
> `ConduitMcp.Transport.SSE`'s `GET /sse`, which is a readable stream.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
