How to selectively allow multiple URL origins in flask?

coding
experience
Python
Flask
Published

July 4, 2021

If you already don’t know what is Cross-Origin Resource Sharing(CORS). Please check out the below links:

IMAGE ALT TEXT

IMAGE ALT TEXT

IMAGE ALT TEXT

IMAGE ALT TEXT

You may have understood the following:

It’s a mechanism to allow communication of one resource to another resource in a different domain. It sets the header, Access-Control-Allow-Origin which can have the following values - *, <origin>, null.

To implement CORS simply in flask, there are a bunch of different ways:

  1. To use flask-cors library
  2. To use a decorator on your own
  3. Others

To use flask-cors library

flask-cors is a library that is like an extension used for handling CORS. It helps in enabling CORS in multiple ways like:

The default way to use flask_cors for all resources in all domains is as follows:

{% highlight python linenos %} from flask import Flask from flask_cors import CORS

app = Flask(name) CORS(app)

@app.route(“/”) def helloWorld(): return “Hello, cross-origin-world!” {% endhighlight %}

To use a decorator on your own

{% highlight python %} # -- coding: utf-8 -- from future import unicode_literals

from datetime import timedelta from flask import make_response, request, current_app from functools import update_wrapper

def crossdomain( origin=None, methods=None, headers=None, expose_headers=None, max_age=21600, attach_to_all=True, automatic_options=True, credentials=False, ): ““” http://flask.pocoo.org/snippets/56/ ““” if methods is not None: methods = “,”.join(sorted(x.upper() for x in methods)) if headers is not None and not isinstance(headers, str): headers = “,”.join(x.upper() for x in headers) if expose_headers is not None and not isinstance(expose_headers, str): expose_headers = “,”.join(x.upper() for x in expose_headers) if not isinstance(origin, str): origin = “,”.join(origin) if isinstance(max_age, timedelta): max_age = max_age.total_seconds()

def get_methods():
    if methods is not None:
        return methods

    options_resp = current_app.make_default_options_response()
    return options_resp.headers["allow"]

def decorator(f):
    def wrapped_function(*args, **kwargs):
        if automatic_options and request.method == "OPTIONS":
            resp = current_app.make_default_options_response()
        else:
            resp = make_response(f(*args, **kwargs))
        if not attach_to_all and request.method != "OPTIONS":
            return resp

        h = resp.headers

        h["Access-Control-Allow-Origin"] = origin
        h["Access-Control-Allow-Methods"] = get_methods()
        h["Access-Control-Max-Age"] = str(max_age)
        if credentials:
            h["Access-Control-Allow-Credentials"] = "true"
        if headers is not None:
            h["Access-Control-Allow-Headers"] = headers
        if expose_headers is not None:
            h["Access-Control-Expose-Headers"] = expose_headers
        return resp

    f.provide_automatic_options = False
    return update_wrapper(wrapped_function, f)

return decorator

{% endhighlight %}

I will prefer to use this implementation if my use case was to just enable CORS instead of importing a third-party library.

Let’s look into some of the headers used in this method:

  • Access-Control-Allow-Origin: It indicates whether the response can be shared with requesting code from the given origin.
  • Access-Control-Allow-Headers: It is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request.
  • Access-Control-Allow-Methods: It specifies the method or methods allowed when accessing the resource in response to a preflight request.

To use this decorator, just import it and use it over any resources in Flask as following:

@app.route("/predict/tree", methods=["POST", "GET"])
@crossdomain(origin='*')
def predict_tree():

Yet both the above approaches are defaulting to set Access-Control-Allow-Origin: * or <origin> as per the HTTP standards.

Others

There maybe a ton of other options as well the writer may not know 😅. Just wanted to put that straight out and I am aware flask-restx cors method is just the code script mentioned in method2.

Yet our issue in plain English is to allow just a small list of allowed origins for our API endpoint. For that, we need to check if the requested resource that is to be shared is part of our list of allowed origins. If yes, set header Access-control-allow-Origin with that requested resource URL.

Trying to implement with flask-cors library

In configuration docs of flask-cors. It’s defined that Origin can be set as a string, List, regex pattern too. When passed as a string, the entire string of whitelist URLs was being set for the header Access-control-Allow-Origins. So this solution doesn’t work, as expected as our requirement.

TADA finally the solution 🤗

For solving the issue, we had to finally rely after_request function. It’s a function used to run after each request. It should always take response as a parameter and should return a new response object or the same one passed. The after_request method doesn’t pass requests in case of any exceptions in program. Check the link if you are interested to know more about flask after_request and it’s friends.

{% highlight python linenos %} from flask import request

@app.after_request def cors_origin(response): allowed_origins = [‘https://kurianbenoy.com’, ‘https://beautifuljekyll.com/’] if allowed_origins == “”: response.headers[‘Access-Control-Allow-Origin’] = ”” else: assert request.headers[‘Host’] if request.headers.get(“Origin”): response.headers[“Access-Control-Allow-Origin”] = request.headers[“Origin”] else: for origin in allowed_origins: if origin.find(request.headers[“Host”]) != -1: response.headers[“Access-Control-Allow-Origin”] = origin return response {% endhighlight %}

On checking multiple websites, I have noticed sometimes some websites don’t have the header Origin or Referer header always. So we first check if there, such an Origin exist, if it exists set the Access-Control-Allow-Origin header as the Origin value, else check if the URL matches the request. headers['Host'], if yes set that URL in the Access-Control-Allow-Origin header.

If not implemented CORS properly, there is a possibility for using CORS misconfigurations for cracking your website like what was done to a few bitcoin brokers. We all should understand that Same Origin Security Policy is a bedrock of web application security.

Some of the exploitable misconfigurations with CORS are:

  1. Reflected Origin in Access-Control-Allow-Origin - Most of the real attacks require Access-Control-Allow-Credentials to be set True, which is a cause of this vulnerability too. Since developers are setting Access-Control-Allow-Origin dynamical they simply copy the value of Origin header. So this vulnerability can be exposed sometimes when developer checks for domain(victimdomain.com) in Allowed Origin header, then attacker use (attackervictimdomain.com) to steal confediential information.
  2. Setting as null origin - The specification mentions it being triggered by redirects, and a few stackoverflow posts show that local HTML files also get it. Perhaps due to the association with local files,it’s commonly used by developers and it can be used to sandbox iframe.

More exploitations and misconfigurations with CORS can be found in these links:

link1

link 2

Hello dear reader! I have been trying to learn Flask and solve this issue(because I am working on a project with it). I’ve written a few hundred lines of code in Flask over the past 2 years, but honestly, I’m pretty bad at it and don’t know the internal workings yet. So my goal with this approach was to learn enough while not getting confused a lot.

I am sharing my learnings on things which I have struggled inspired by Julia Evans. Please let me know if there is any better approach for this. Thanks for reading 🙏.