Draft CORS guidance for an IVOA JSON protocol (was: x-www-form-urlencoded prohibition)
Russ Allbery
eagle at eyrie.org
Thu May 23 21:18:10 CEST 2024
If one isn't that familiar with CORS already, it may be hard to follow the
discussion in my previous message about how to use CORS with a
hypothetical future RESTful JSON protocol. We also need to start drafting
guidance at some point anyway, so I may as well give it a shot now to make
this more concrete.
The following applies only to a protocol that forces browser pre-flight
checks by default. Examples include protocols that require Authorization
headers or require the Content-Type of the body be application/json.
As with anything related to CORS, this does not apply to GET unless extra
headers are required. The standard web service guidance is to not use GET
for anything that would be risky if unauthenticated users were able to
blindly trigger a GET but not see the response.
There are three basic cases:
1. The service is unauthenticated. CSRF concerns do not apply and you
should use an open CORS policy. To do this, register an OPTIONS
handler for every API route (usually one catch-all OPTIONS handler is
sufficient if you don't have different policies per route) that
responds with a 204 code and the following headers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
You may also want to send Access-Control-Allow-Headers if your web
service accepts non-simple-request headers, and
Access-Control-Expose-Headers if your service returns extra headers
that should be readable by JavaScript.
This is a very common use case and many web frameworks support setting
this up for you with a few lines of code. [1]
2. The service is authenticated, but you do not believe that any of the
operations that it supports are sufficiently risky to warrant concern
about CSRF. In practice, this means that the service doesn't provide
destructive commands (or you accept the risk of someone's browser being
tricked into issuing them), and you aren't concerned about denial of
service attacks (which is a reasonable position for most sites).
Similar to case 1, in this case CSRF concerns do not apply and you
should use an open CORS policy. You have to do this slightly
differently than case 1 because you have to allow the browser to send
you credentials. Standard headers in your OPTIONS response look like:
Access-Control-Allow-Origin: <copy of the Origin header in request>
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Credentials: true
(Yes, this means that, in the authenticated case, OPTIONS handlers
cannot return a static response and need to vary the response based on
the request.) As above, you may need to add additional headers.
As with case 1, this is a common pattern and many web frameworks will
set this up for you, although they may want a whitelist of origins (see
case 3b) so you may have to write a bit of code to allow any origin.
3. The service is authenticated and you don't want to allow any web site
with JavaScript to drive your service. There are four common subcases:
a. You do not want to allow remote web sites to drive your service, or
at least you don't care enough about this use case to want to
support it. Do nothing; the default CORS policy will work fine for
you. Other web sites won't be able to drive your service with
client-side JavaScript, but direct requests from anywhere on the
Internet (with the correct authentication credentials) will work
without any further action required on your part.
b. You want to allow a specific whitelisted set of remote web sites to
drive your service. In this case, add that whitelist to your
configuration and, when you receive an incoming OPTIONS request,
check the Origin header against that whitelist. If it matches,
respond as in case 2. If it does not, reply with a 400 error. This
is another very common use case that is probably supported by your
web framework.
c. You want to allow every well-known astronomical portal to drive your
API service, but not random sites on the Internet. This is the case
where some IVOA registry work would be useful. If this turns out to
be a common desired policy, we could provide a mechanism for sites
to register the origins (in the specific technical sense of the
Origin HTTP header) of their portals that support cross-site IVOA
protocol requests, and IVOA services could retrieve this list of
origins from the registry and configure their CORS OPTIONS responses
to whitelist those origins using a mechanism similar to 3b.
d. You want to allow users to whitelist any web site that they use to
drive your API, but you don't want to allow any random web site to
drive your API without an authenticated user explicitly whitelisting
it. This is the hardest case and will require a bit of effort. You
could, for example, provide users with a configuration screen where
they could add origins (generally https plus the hostname) of the
portals they use to a local database, and then the service would
allow any origin listed in that database. Or you could go further
and provide some OAuth-style dynamic registration service for web
portals to register themselves with your services as valid origins.
A lot could be done here, but it's effort, so I would want to wait
to see how often this use case arises before doing design work. I
suspect this use case may be relatively rare if we can provide a
nice solution for case 3c. In the initial draft guidance, I think
the place to start is to provide some vague guidance about providing
the user with a way to enter hostnames of web portals they use.
For the case of x-www-form-urlencoded POST, cases 1 and 2 are the default
behavior provided that the site doesn't expect or send extra HTTP headers
and provided that cookies are used for authentication. If the API
requires any headers that aren't whitelisted for simple requests
(Authorization is the most common), the browser will do CORS preflight and
the service will still have to implement the above logic.
If case 3 applies but the service also needs to support cookie
authentication without any extra HTTP headers, then one of the other CORS
mitigation strategies must be used. This is the case that I think is
worth documenting in a standardization of the x-www-form-urlencoded
network encoding. The obvious thing to document is probably some version
of the synchronizer token pattern [2], although in some cases it may be
possible to get away with using SameSite=Strict cookies. (SameSite=Strict
cookies have some significant limitations, however, which would deserve a
longer discussion.) Documenting this as part of the standard would allow
use of an authenticated, CSRF-protected x-www-form-urlencoded POST API by
non-browser clients such as PyVO or TOPCAT.
One final note: CORS protection does not apply to same-origin requests.
Any JavaScript running within the same origin as your API service (in
practice, this means scheme, hostname, and port) can make authenticated
requests to your API service without regard to your CORS policy.
It is possible to try to add mitigations if you have untrusted sites in
the same origin, but this is strongly discouraged by all standard web
security advice. The browser JavaScript security boundary is the origin;
if you have web sites with different trust levels, they should run in
different origins. This is the advice we should give in IVOA standards.
This means that many cases that superficially appear to be case 3a turn
out to be case 3b, because there's often a reason to serve the web UI from
a different origin from the API.
[1] https://fastapi.tiangolo.com/tutorial/cors/#use-corsmiddleware, for
example, for FastAPI. The documentation discourages case 2 for the
normal web security reasons, but it does support case 2 with no
additional code by using allow_origin_regex=r".*".
[2] https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
--
Russ Allbery (eagle at eyrie.org) <https://www.eyrie.org/~eagle/>
More information about the grid
mailing list