When and how to use CORS

I am reviewing an http endpoint in an old app, I see this code and I’m having some doubts about how CORS works.

Here is the code:

@anvil.server.http_endpoint('/header/:name')
def http_header(name):
    data = something()
    return response(data)

def response(data):
    response = anvil.server.HttpResponse(200, data)
    allowed_origins = ["http://domain1", "https://domain2"]
    if anvil.server.request.origin in allowed_origins:
        response.headers["Access-Control-Allow-Origin"] = anvil.server.request.origin
    return response

It looks like response takes care of adding the correct CORS header and returns the response.

If this is the case, I have two questions:

  • Is it safe/wise to first execute the resource expensive something, then check if I should have done it later?
  • Is it safe to return a response with the payload and a header asking the client “please be nice, and if you are not who you should be, please ignore the info I just sent you”?
  • What is the difference between the code above and the following?
@anvil.server.http_endpoint('/header/:name')
def http_header(name):
    allowed_origins = ["http://domain1", "https://domain2"]
    if anvil.server.request.origin not in allowed_origins:
        return 'Origin not allowed'
    return something()

It depends on what type of HTTP request it is. PUT and DELETE requests will make an OPTIONS request for permission before the actual request. For those you need to detect the OPTIONS request and return the right headers instead of doing the actual work. The actual work would be for if the request is not an OPTIONS request.

Since you’re not regularly getting something() executed twice (I’m assuming), then I’d say the only change to make is to check the allowed origins first, on the principle you shouldn’t do the work if it isn’t from an allowed origin.

My understanding of CORS is limited to getting endpoints working when called from Javascript code, so the above might be flawed.

I had only GET in my mind, for my use case, thanks for mentioning the other methods.

Do you know what the code in my first snippet does when the request comes from an unallowed domain?

I know it does waste time doing something, and I know it does try to send the payload, but I don’t know if that payload is blocked by the server somewhere between my function and the lower level http protocol handling, or if it is sent to the client.

If I need to write some code to check whether the domain is trusted and modify the response accordingly, wouldn’t it be better to use the same check logic and return a 401 (Unauthorized) response without payload rather than a 200 response with the payload and the CORS header?

In other words, what is CORS for, if, in order for it to work, I need to add some logic in my code?

CORS is handled by the browser, so your response would go back to the browser and get rejected there.

CORS does require cooperation from both client and server to work. The server has to set the list of allowed domains, the client then accepts or rejects the response based on that.

That sounds like a good approach to me.

CORS allows browsers to protect against malicious Javascript injected into an app. Your app from example.com will not be able to make HTTP requests to financialsite.com unless financialsite.com says it can. So it’s up to the server to say who can make those requests.

1 Like

I read (i.e., had a lengthy conversation with ChatGPT) and played with CORS, and I now understand it a bit better than I did yesterday.

Here are the things I understand, please let me know if I am wrong.

  1. The code in the old app, shown in the first snippet, is useless. That code does nothing. Has been there for 6 years, doing nothing for 6 years. Yay!

    The reason why I have never noticed that it did nothing is that I was calling that HTTP endpoint from the server, not from the client. I initially included it because I didn’t realize that CORS comes into play when a page served by server 1 tries to fetch something from server 2, not when server 1 tries to fetch something from server 2.

  2. Anvil HTTP endpoints are not accessible by other site pages because they don’t respond to preflight OPTIONS requests unless the decorator includes these two arguments:

    @anvil.server.http_endpoint("/xxx/:param", cross_site_session=True, methods=['GET', 'OPTIONS']), and the headers are correctly managed for every method.

Here’s a working HTTP endpoint that handles CORS requests properly:

@anvil.server.http_endpoint("/xxx/:param", cross_site_session=True, methods=['GET', 'OPTIONS'])
def http_xxx(param):
    print(f"xxx method:{anvil.server.request.method} origin:{anvil.server.request.origin}")
    allowed_origins = ["http://domain1", "https://domain2"]

    if anvil.server.request.method == "OPTIONS":
        # Handle preflight request
        response = anvil.server.HttpResponse(200)
        if anvil.server.request.origin in allowed_origins:
            response.headers["Access-Control-Allow-Origin"] = anvil.server.request.origin
            response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
            response.headers["Access-Control-Allow-Headers"] = "Content-Type"
        return response

    # Handle actual request
    if anvil.server.request.origin not in allowed_origins:
        return anvil.server.HttpResponse(403, "Origin not allowed")

    data = f'Hello {param}'
    
    response = anvil.server.HttpResponse(200, json.dumps(data))
    response.headers["Access-Control-Allow-Origin"] = anvil.server.request.origin
    response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
    response.headers["Access-Control-Allow-Headers"] = "Content-Type"
    response.headers["Content-Type"] = "application/json"
    return response

Here is what I see in the log after sending 2 requests, one from an allowed domain and one from a disallowed one:

  • At 15:25:20 the preflight OPTIONS request came from an unallowed site, a response without the correct CORS header was returned, so the browser never sent the GET request
  • At 15:25:37 the preflight OPTIONS request came from an allowed site, a response with the correct CORS header was returned, so the browser did send the GET request

This endpoint can only be used for cross site requests from the specified domains. If it’s not called from a browser, the origin will be undefined, resulting in a 403 response.

By removing the check that returns the 403 response, this endpoint can be used for both cross-site requests from allowed origins and requests not originating from browsers.

The log shows an empty entry next to each entry handled by the endpoint. These empty entries may be tied to the underlying CORS mechanism.

2 Likes

That all seems reasonable to me.