A simple explanation of CORS

Sep 23, 2020 · 733 words · 4 minute read CORS python

It’s a well known fact that software developers try to eliminate repetitive work with automation. In our company, admins of services that are responsible for identity and access management receive multiple requests for resource creation every day.

Most of these are really similar and do not violate our security policy. So to save time, we develop trimmed down versions of REST APIs for previously mentioned services so other departments can create and manage basic resources themselves. All endpoints in this new REST API must first be reviewed by our security department.

Imagine person who likes to execute HTTP requests from REST client running in browser (e.g. Swagger UI). None of these requests would execute properly. The reason of this failure is CORS. Moreover it would not be possible to use this REST API as a backend for some website.

Most of the web pages you visit daily, load their assets from different servers. If browser knows which servers are necessary to load current website, than it is easier to prevent hijacking and downloading malicous code. All of this can be accomplished thanks to CORS (“Cross-origin resource sharing”). Every modern web browser enforces it. All HTTP requests, except GET, HEAD and POST which contain custom HTTP headers or specific standard content type are considered not secure. If you make a request like this, browser tries to verify, if it is secure enough. The verification process consists of preflight request, that check what methods, headers and origins requested server supports. Demanded request will be executed only if it fullfills specification returned by successfully executed preflight request.

To solve this, it is advised to use open-source libraries, that are created and maintaned by folks who really understand CORS. In order to understand this mechanism a bit more on our own, let’s code the CORS support ourselves.

Below is a minimal REST API written in Tornado Framework in Python. It contains only one endpoint TestEndpoint which returns success.

import tornado.web
import tornado.ioloop


class TestEndpoint(tornado.web.RequestHandler):
    def get(self):
        self.write("success")

if __name__ == "__main__":
    app = tornado.web.Application([
        (r"/", TestEndpoint)
    ])

    port = 8888
    app.listen(port)
    print("Successfully started")
    tornado.ioloop.IOLoop.current().start()

Let’s start our development server locally and try to call HTTP GET request from browser. Browser doesn’t like our CORS implementation - maybe that’s because we didn’t do any :D.

Missing Access-Control-Allow-Origin

CORS mechanism consists of a preflight request. Method of this request is OPTIONS and it asks the server about allowed origins, headers and other information like credentials support. In the code, our endpoint supports only GET request, so when browser sends a preflight request, which has OPTIONS as its method, it receives no answer, because our endpoint doesn’t know how to react. If we closely look at error response specified in browser, it says that CORS header with name ‘Access-Control-Allow-Origin’ is missing. So let’s create a function which will return status code 204 (‘No Content’) and add a header with the name ‘Access-Control-Allow-Origin’ and set it to * (wildcard). Setting ‘Access-Control-Allow-Origin’ to Wildcard means, that resource can be accessed by any domain. If you wish to allow only specific origins, you can specify the domain names of allowed origins.

Endpoint with the name TestEndpoint now supports OPTIONS method.

import tornado.web
import tornado.ioloop


class TestEndpoint(tornado.web.RequestHandler):
    def get(self):
        self.write("success")

    def options(self):
        self.set_header("Access-Control-Allow-Origin",
                        "*")
        self.set_status(204)
        self.finish()

if __name__ == "__main__":
    app = tornado.web.Application([
        (r"/", TestEndpoint)
    ])

    port = 8888
    app.listen(port)
    print("Successfully started")
    tornado.ioloop.IOLoop.current().start()

So let’s restart the development server and execute HTTP GET request once again. As we can see, the error response code has changed.

Missing Access-Control-Allow-Headers

Browser now signals an issue with allowed headers. So lets specify which headers we would like to allow on server side. This is implemented by the addition of header with name ‘Access-Control-Allow-Headers’, but now it is not possible to set wildcard * as its value. Because of security reasons, it is necessary to specify explicit headers. If endpoint requires multiple custom headers, it is necessary to separate header names with comma.

    def options(self):
        self.set_header("Access-Control-Allow-Origin",
                        "*")
        self.set_header("Access-Control-Allow-Headers",
                        "Content-Type")
        self.set_status(204)
        self.finish()

After final update of the options function, lets restart the server once again and execute the same request.

Success

Finally we have arrived at the correct response.

As you can see, it is easy to comprehend the basic flow of this mechanism.

List of some open-source libraries for CORS management :

Samuel Braniša
Technical Security