CORS¶
A page on another origin can only call the app from a browser if the app says it may.
Options¶
| option | default | |
|---|---|---|
allow_origins |
required | exact origins, or ["*"] for any |
allow_methods |
["*"] |
methods a preflight may ask for |
allow_headers |
["*"] |
request headers a preflight may ask for |
allow_credentials |
False |
let the browser send cookies and Authorization |
expose_headers |
[] |
response headers a page's script may read |
max_age |
600 |
seconds a browser may cache a preflight answer |
An origin is a scheme, host and optional port with no path and no trailing
slash — https://app.example.com, http://localhost:5173 — exactly as the
browser sends it in the Origin header.
allow_origins has no default because it is the decision that matters. Methods
and headers default to any: once an origin is trusted, permitting a method or a
header does not widen what that origin can do. The origin list and
allow_credentials are the boundary.
Credentials¶
With credentials, the browser sends cookies and Authorization on cross-origin
requests, and the app's responses are readable by that origin's scripts.
allow_credentials=True with allow_origins=["*"] raises. It would let every
website make requests as a signed-in user and read the answers. Browsers refuse
a literal * with credentials, and a server that echoes back whatever origin
arrives to get around that is exactly the vulnerability. List the origins.
What the server does¶
On every response, to a request whose Origin is allowed:
Access-Control-Allow-Origin — the origin itself, or * when any origin is
allowed without credentials — plus Access-Control-Allow-Credentials and
Access-Control-Expose-Headers when configured. Vary: Origin is added
whenever the answer depends on the origin, including on responses to requests
with no Origin at all, so a shared cache never hands one origin's answer to
another.
On a preflight — OPTIONS carrying Origin and
Access-Control-Request-Method — the server answers 204 with the allowed
method, headers and Access-Control-Max-Age, or 400 with the reason when the
origin, method or a header is not allowed. A preflight is answered for any path,
whether or not a route exists there, so a foreign site cannot use preflights to
discover which paths exist. A plain OPTIONS without those headers is routed
like any other request.
A request from a foreign origin is still served; its response simply carries no CORS headers, and the browser withholds it from the page. CORS is enforced by the browser. It is not access control, and does nothing against a client that is not a browser.
A header a handler sets itself — Access-Control-Allow-Origin on its own
Response — is left alone.
Why it is not middleware¶
The server answers 404, 405, 413, 422, 503 and 504 itself, without
waking a worker. Middleware never sees those responses. A browser that receives
one without CORS headers reports a CORS error, not the real status, and whoever
is debugging it starts in the wrong place. CORS is applied in Rust so that every
response carries the headers, whoever produced it, and a preflight never costs a
worker.
With no cors configured, none of this runs and nothing is added.
WebSockets¶
Browsers do not apply CORS to WebSocket connections, so the server checks a
socket's Origin separately, before the handshake. By default the origins in
allow_origins may open sockets, along with the app's own origin; * does not
extend to sockets. Set App(websocket_origins=[...]) to choose a different
list. See which sites may connect.