Flevo CFD

Cross-Origin Resource Sharing (CORS): What is it? How does it work?

CORS is a security mechanism that allows web applications to make cross-origin requests.

Cross-origin requests are requests that are made from one domain to another domain.

By default, web browsers prevent cross-origin requests for security reasons. However, CORS allows web applications to make cross-origin requests by adding specific headers to the HTTP response.

CORS relaxes the same-origin policy to enable web applications interact with each other in a secure manner, while not protecting web applications from specific attacks.

Browser enforces CORS to XMLHttpRequest, Fetch API, Web Fonts and a few others.

What’s an Origin

An origin is defined by the scheme, host, and port of a URL. For example, the origin of https://example.com:8080 is https://example.com:8080. it’s basically the value you see in the address bar of your browser.

What’s the Same-Origin Policy

The same-origin policy is a security feature of web browsers that prevents web applications from making cross-origin requests.

The same-origin policy restricts web applications to only make requests to the same origin as the web application. This helps prevent attackers from making unauthorized requests to a web application.

What’s a Cross-Origin Request

A cross-origin request is a request that is made from one origin to another origin. For example, if you have a web application running on https://example.com that makes a request using one of the mentioned elements above for instance a Fetch to https://api.foo.com, this is a cross-origin request.

How Does CORS Work

When a web application makes a cross-origin request, the browser sends a preflight request to the server to check if the server allows the request.

The preflight request is an HTTP OPTIONS request that includes the Origin header. The server responds with the Access-Control-Allow-Origin header, which specifies which origins are allowed to make cross-origin requests.

If the server allows the request, the browser sends the actual request. If the server does not allow the request, the browser blocks the request.

If the request is a simple request, the browser sends the actual request directly without sending a preflight request. The preflight request is to check if the actual request is safe to send and does not have side effects.

A simple request is a request that meets the following criteria

  • The request method is GET, HEAD, or POST.
  • Apart from the headers that the browser always sends, the only headers that the request can include are Accept, Accept-Language, Content-Language, Content-Type and Range
  • The Content-Type header is application/x-www-form-urlencoded, multipart/form-data, or text/plain.

Note that not sending a preflight request does not mean that the CORS policy is not enforced. The browser still enforces the CORS policy for simple requests.

I said earlier that since a simple request is safe, the browser does not send a preflight request. However, you saw that a request with a POST method could be a simple request and we know that a POST request could change state of server. This is to allow HTML forms to submit data to a different origin. CORS is not meant to enforce restrictions but to relax the same-origin policy.

a Simple Application to Demonstrate CORS

Let’s setup a simple application to demonstrate CORS. We will have two flask applications, one running on http://devbox:3000 and the other running on http://devbox:4000.

We’re using linux hosts file to serve the application under a domain name. hosts file is a handy way to map domain names to IP addresses. In a production environment, you would use a DNS server to map domain names to IP addresses. It’s not needed for the demonstration tho.

1
sudo bash -c 'echo "127.0.0.1 devbox" >> /etc/hosts'

devbox 1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# main.py
from flask import Flask


app = Flask(__name__)


@app.route("/test/")
def test():
    return {"status": "it works!"}
1
flask --app main run -p 3000 --reload

It’s accessible at http://devbox:3000/test/

devbox 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# main.py
from flask import Flask, make_response, render_template


app = Flask(__name__)


@app.route("/")
def index():
    resp = make_response(render_template("index.html"))  
    return resp
1
2
3
4
mkdir templates
cat <<EOF > templates/index.html
<script>fetch('http://devbox:3000/test/').then(res => res.json()).then(console.log)</script>
EOF
1
flask --app main run -p 4000 --reload

Now if you open http://devbox:4000 in your browser, you will see the following error in the console

Access to fetch at 'http://devbox:3000/test/' from origin 'http://devbox:4000' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header
is present on the requested resource. If an opaque response serves your needs,
set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

This error occurs because the browser blocks the request from http://devbox:4000 to http://devbox:3000 due to the same-origin policy. To fix this error, we need to enable CORS on the server running on http://devbox:3000.

1
2
3
4
5
@app.route("/test/")
def test():
    resp = make_response({"status": "it works!"})
    resp.headers["Access-Control-Allow-Origin"] = "http://devbox:4000"
    return resp

It should work now.

If withCredentials is set to true in the request, the server must also include the Access-Control-Allow-Credentials header in the response with the value true.

When you visit a website, the browser always sends cookies that are associated with the website to the server. This allows the website to identify you and provide personalized content.

However, when you make a cross-origin request, the browser does not send cookies and HTTP authentication information by default. You can enable this behavior by setting the withCredentials flag to true in the request.

Browser also checks other headers like Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Max-Age and Access-Control-Expose-Headers to determine if the request is allowed. this depends on the request method and headers.

The previous request didn’t trigger a preflight request because it was a simple request. If the request was a non-simple request, the browser would have sent a preflight request to the server to check if the server allows the request.

Let’s make the request a non-simple request by adding a custom header to the request.

1
2
3
cat <<EOF >| templates/index.html
<script>fetch('http://devbox:3000/test/', {headers: {'X-Custom-Header': 'value'}}).then(res => res.json()).then(console.log)</script>
EOF

Now if you open http://devbox:4000 in your browser, you should see the following error in the console

1
2
3
4
5
Access to fetch at 'http://devbox:3000/test/' from origin 'http://devbox:4000'
has been blocked by CORS policy: Response to preflight request doesn't
pass access control check: No 'Access-Control-Allow-Origin' header
is present on the requested resource. If an opaque response serves your needs,
set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Unlike the simple request, the browser sends a preflight request to the server to check if the server allows the request. The server must respond with the Access-Control-Allow-Origin header to allow the request.

To fix the issue, we need to update the application to respond the preflight request which is an HTTP OPTIONS request.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from flask import request


@app.route("/test/", methods=["GET", "OPTIONS"])
def test():
    if request.method == "OPTIONS":
        resp = make_response()
        resp.headers["Access-Control-Allow-Origin"] = "http://devbox:4000"
        return resp

    resp = make_response({"status": "it works!"})
    resp.headers["Access-Control-Allow-Origin"] = "http://devbox:4000"
    return resp

Let’s try again.

Access to fetch at 'http://devbox:3000/test/' from origin 'http://devbox:4000'
has been blocked by CORS policy: Request header field x-custom-header is not
allowed by Access-Control-Allow-Headers in preflight response.

We need to update the server to allow the X-Custom-Header header in the preflight response.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@app.route("/test/", methods=["GET", "OPTIONS"])
def test():
    if request.method == "OPTIONS":
        resp = make_response()
        resp.headers["Access-Control-Allow-Origin"] = "http://devbox:4000"
        resp.headers["Access-Control-Allow-Headers"] = "X-Custom-Header"
        return resp

    resp = make_response({"status": "it works!"})
    resp.headers["Access-Control-Allow-Origin"] = "http://devbox:4000"
    return resp

Note that Access-Control-Allow-Headers should be present in both the preflight response and the actual response.

What Attacks Does CORS Protect Against

CORS does not protect against specific attacks. It is a security mechanism that allows web applications to make cross-origin requests. However, CORS can help prevent certain types of attacks, such as cross-site request forgery (CSRF) attacks.

By restricting which origins are allowed to make cross-origin requests, CORS can help prevent attackers from making unauthorized requests to a web application.

Check out the MDN documentation for more information.