The Origin check of the MCP transport, against DNS rebinding.
A request without an Origin header (every client that is not a browser)
passes. A request with one must name a host this install knows, else it is
answered 403 origin_not_allowed before any key is looked at (the MCP plug
runs this as the :before_auth hook of PhoenixKit.API.Auth).
What is allowed
- the install's canonical host: the host of the
site_urlsetting, else of the endpoint's URL (the same source asPhoenixKit.Utils.Routes.base_url/0, read through the settings cache because this check runs before any rate limit); - when the host app configures
config :phoenix_kit, :api_request_context, {Module, :function}, the:hoststhat function explicitly returns (every domain of a multi-domain install). A hook that returns no:hostskey (a hook that sets only language and currency), an empty or non-string list, junk, or that raises or throws contributes nothing. The hook is called with the conn, and it runs again later in the same request when the request context is resolved, so it must be cheap and free of side effects; - the entries of the
api_mcp_allowed_originssetting, which are full origins:https://client.examplematches scheme, host and port. An entry without://never matches (the Settings page stores only full origins).
The request's own Host header is deliberately not on the list. A
DNS-rebinding page sends Host: evil.test and Origin: http://evil.test;
both come from the attacker, so a list built from the Host header would
admit it. The default PhoenixKit.API.RequestContext host list is exactly
that header, and RequestContext.resolve/1 keeps it whenever a hook returns
no :hosts or fails, which is why this module does not use resolve/1 and
calls the hook itself.
An origin with userinfo, a path, a query, a fragment, a scheme other than
http or https, no host (null, file://), surrounding whitespace or
bytes that are not valid UTF-8 is never allowed. A request with two Origin
headers is refused.
Summary
Functions
Whether origin (the value of an Origin header) is allowed, given the
hosts this install serves and the configured entries of
api_mcp_allowed_origins.
The canonical origin, scheme://host[:port]: that of the site_url setting
(read through the settings cache), else of the endpoint's URL, else of the
static configuration, else http://localhost:4000. A rung that is set but
unusable (no scheme, not http(s), no host) is skipped. The host is
lowercase and a default port is left out.
Passes conn when it has no Origin header or an allowed one; otherwise
sends 403 origin_not_allowed and halts.
The hostnames this install is known to serve, lowercase and without ports:
the canonical host first, then the hosts the :api_request_context hook
explicitly returns (see the moduledoc). The request's own Host header is
never on the list.
origin (as canonical_origin/0 returns it) with host in place of its
host: the scheme and the port are kept.
Functions
Whether origin (the value of an Origin header) is allowed, given the
hosts this install serves and the configured entries of
api_mcp_allowed_origins.
@spec canonical_origin() :: String.t()
The canonical origin, scheme://host[:port]: that of the site_url setting
(read through the settings cache), else of the endpoint's URL, else of the
static configuration, else http://localhost:4000. A rung that is set but
unusable (no scheme, not http(s), no host) is skipped. The host is
lowercase and a default port is left out.
The last two rungs mean the install never said what its public address is: that is logged once per boot. It never raises.
The same source as PhoenixKit.Utils.Routes.base_url/0, but without its
uncached database read, so a public request can use it.
@spec check(Plug.Conn.t()) :: Plug.Conn.t()
Passes conn when it has no Origin header or an allowed one; otherwise
sends 403 origin_not_allowed and halts.
@spec known_hosts(Plug.Conn.t()) :: [String.t()]
The hostnames this install is known to serve, lowercase and without ports:
the canonical host first, then the hosts the :api_request_context hook
explicitly returns (see the moduledoc). The request's own Host header is
never on the list.
Public for the other documents the install publishes about itself
(PhoenixKit.API.PublicDiscovery), which must not trust the Host header
either.
origin (as canonical_origin/0 returns it) with host in place of its
host: the scheme and the port are kept.